From cf38e81844d6828825c0cf5810cc9186b58174f9 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Wed, 30 Sep 2020 17:56:39 +0200 Subject: Sheet container: show statistics --- archaeological_warehouse/models.py | 409 ++++++++++++--------- .../templates/ishtar/sheet_container.html | 119 +++++- .../templates/ishtar/sheet_warehouse.html | 34 +- 3 files changed, 368 insertions(+), 194 deletions(-) diff --git a/archaeological_warehouse/models.py b/archaeological_warehouse/models.py index a8a38c34d..ffc491ddc 100644 --- a/archaeological_warehouse/models.py +++ b/archaeological_warehouse/models.py @@ -45,6 +45,201 @@ from ishtar_common.utils import cached_label_changed, \ cached_label_and_geo_changed +class DivisionContainer(DashboardFormItem): + DIVISION_TEMPLATE = """ + + {container} {ref}""" + BASE_QUERY_LOCATION = "location" + + @property + def pk(self): + # of course implemented by models.Model + raise NotImplemented + + def get_max_division_number(self): + raise NotImplemented + + @property + def start_division_number(self): + raise NotImplemented + + @property + def division_labels(self): + if not self.get_max_division_number() + 1: + return [] + start = self.start_division_number + return [ + "{} {}".format(_("Division"), idx + start + 1) + for idx in range(self.get_max_division_number() + 1) + ] + + @property + def number_divisions(self): + q = { + self.BASE_QUERY_LOCATION + "__id": self.pk, + "container_type__stationary": True + } + return Container.objects.filter(**q).count() + + @property + def number_containers(self): + q = { + self.BASE_QUERY_LOCATION + "__id": self.pk, + "container_type__stationary": False + } + return Container.objects.filter(**q).count() + + @property + def number_of_finds_hosted(self): + Find = apps.get_model("archaeological_finds", "Find") + q = { + "container__{}__id".format(self.BASE_QUERY_LOCATION): self.pk, + } + return Find.objects.filter(**q).count() + + @property + def number_of_finds(self): + Find = apps.get_model("archaeological_finds", "Find") + q = { + "container_ref__{}__id".format(self.BASE_QUERY_LOCATION): self.pk, + } + return Find.objects.filter(**q).count() + + @property + def number_of_containers(self): + return Container.objects.filter( + **{self.BASE_QUERY_LOCATION: self}).count() + + def _number_of_finds_by_place(self): + Find = apps.get_model("archaeological_finds", "Find") + return self._number_of_items_by_place( + Find, division_key='inside_container__container__') + + @property + def number_of_finds_by_place(self, update=False): + return self._get_or_set_stats('_number_of_finds_by_place', update) + + def _number_of_containers_by_place(self): + return self._number_of_items_by_place( + ContainerTree, 'container_parent__', 'container__children') + + @property + def number_of_containers_by_place(self, update=False): + return self._get_or_set_stats('_number_of_containers_by_place', update) + + def _get_divisions(self, current_path, remaining_division, depth=0): + if not remaining_division: + return [current_path] + remaining_division.pop(0) + + query_location = self.BASE_QUERY_LOCATION + for __ in range(depth): + query_location = "parent__" + query_location + base_q = Container.objects.filter(**{query_location: self}) + + q = base_q + if self.BASE_QUERY_LOCATION == 'location': + exclude = "parent_" + for idx in range(depth): + exclude += "_parent_" + q = base_q.filter(**{exclude + "id": None}) + elif not depth and not current_path: + q = base_q.filter(parent_id=self.pk) + + for idx, p in enumerate(reversed(current_path)): + parent_id, __ = p + key = "parent__" * (idx + 1) + "id" + q = q.filter(**{key: parent_id}) + res = [] + old_ref, ct = None, None + if not q.count(): + return [current_path] + q = q.values( + 'id', 'reference', 'container_type__label', 'container_type_id' + ).order_by('container_type__label', 'reference') + + for ref in q.all(): + if ref['reference'] == old_ref and \ + ref["container_type__label"] == ct: + continue + old_ref = ref['reference'] + ct = ref["container_type__label"] + cpath = current_path[:] + lbl = self.DIVISION_TEMPLATE.format( + id=ref["id"], container=ref["container_type__label"], + ref=ref['reference']) + cpath.append((ref["id"], lbl)) + query = { + "containers__parent__reference": ref['reference'], + "containers__parent__container_type_id": ref[ + "container_type_id"], + "containers__" + self.BASE_QUERY_LOCATION: self + } + remaining_division = list( + ContainerType.objects.filter( + **query).distinct()) + for r in self._get_divisions(cpath, remaining_division[:], + depth + 1): + res.append(r) + return res + + @property + def available_division_tuples(self): + """ + :return: ordered list of available paths. Each path is a list of + tuple with the container type and the full reference. + """ + q = { + "containers__" + self.BASE_QUERY_LOCATION: self + } + if self.BASE_QUERY_LOCATION == 'location': + q["containers__parent"] = None + top_divisions = list( + ContainerType.objects.filter(**q).distinct()) + divisions = self._get_divisions([], top_divisions) + return divisions + + def _number_of_items_by_place(self, model, division_key, count_filter=None): + res = {} + paths = self.available_division_tuples[:] + for path in paths: + cpath = [] + for container_id, lbl in path: + cpath.append((container_id, lbl)) + if tuple(cpath) in res: + continue + q = model.objects + for idx, p in enumerate(reversed(cpath)): + container_id, __ = p + div_key = division_key + "parent__" * idx + attrs = { + div_key + "id": container_id + } + q = q.filter(**attrs) + if count_filter: + q = q.filter(**{count_filter: None}) + res[tuple(cpath)] = q.distinct().count() + res = [(k, res[k]) for k in res] + final_res, current_res, depth = [], [], 1 + + len_divisions = self.get_max_division_number() + 1 + for path, nb in sorted(res, key=lambda x: (len(x[0]), x[0])): + if len(path) > len_divisions: + continue + if depth != len(path): + final_res.append(current_res[:]) + current_res = [] + depth = len(path) + if path[-1] == '-': + continue + path = [k[1] for k in path] + path = path + ['' for __ in range(len_divisions - len(path))] + current_res.append((path, nb)) + final_res.append(current_res[:]) + return final_res + + class WarehouseType(GeneralType): class Meta: verbose_name = _("Warehouse type") @@ -56,8 +251,8 @@ post_save.connect(post_save_cache, sender=WarehouseType) post_delete.connect(post_save_cache, sender=WarehouseType) -class Warehouse(Address, DocumentItem, GeoItem, QRCodeItem, DashboardFormItem, - OwnPerms, MainItem): +class Warehouse(Address, DocumentItem, GeoItem, QRCodeItem, + OwnPerms, MainItem, DivisionContainer): SLUG = 'warehouse' APP = "archaeological-warehouse" MODEL = "warehouse" @@ -155,28 +350,6 @@ class Warehouse(Address, DocumentItem, GeoItem, QRCodeItem, DashboardFormItem, def __str__(self): return self.name - @property - def number_divisions(self): - return Container.objects.filter( - location=self.pk, container_type__stationary=True).count() - - @property - def number_containers(self): - return Container.objects.filter( - location=self.pk, container_type__stationary=False).count() - - @property - def number_finds(self): - Find = apps.get_model("archaeological_finds", "Find") - return Find.objects.filter( - container__location_id=self.pk).count() - - @property - def number_owned_finds(self): - Find = apps.get_model("archaeological_finds", "Find") - return Find.objects.filter( - container_ref__location_id=self.pk).count() - @property def short_label(self): return self.name @@ -215,14 +388,12 @@ class Warehouse(Address, DocumentItem, GeoItem, QRCodeItem, DashboardFormItem, setattr(self, k, None) self.save() + def get_max_division_number(self): + return self.max_division_number + @property - def division_labels(self): - if not self.max_division_number: - return [] - return [ - "{} {}".format(_("Division"), idx + 1) - for idx in range(self.max_division_number) - ] + def start_division_number(self): + return 0 @property def default_location_types(self): @@ -246,145 +417,6 @@ class Warehouse(Address, DocumentItem, GeoItem, QRCodeItem, DashboardFormItem, def _get_query_owns_dicts(cls, ishtaruser): return [{'person_in_charge__ishtaruser': ishtaruser}] - @property - def number_of_finds(self): - from archaeological_finds.models import Find - return Find.objects.filter(container_ref__location=self).count() - - @property - def number_of_finds_hosted(self): - from archaeological_finds.models import Find - return Find.objects.filter(container__location=self).count() - - @property - def number_of_containers(self): - return Container.objects.filter(location=self).count() - - def _get_divisions(self, current_path, remaining_division, depth=0): - if not remaining_division: - return [current_path] - remaining_division.pop(0) - - query_location = "location" - for __ in range(depth): - query_location = "parent__" + query_location - base_q = Container.objects.filter(**{query_location: self}) - - q = base_q.annotate(nb_children=Count("children__id")).exclude( - nb_children=0) - - if not current_path: - q = q.annotate(nb_parent=Count("parent__id")).filter( - nb_parent=0) - - for idx, p in enumerate(reversed(current_path)): - parent_id, __ = p - key = "parent__" * (idx + 1) + "id" - q = q.filter(**{key: parent_id}) - res = [] - old_ref, ct = None, None - if not q.count(): - return [current_path] - q = q.values( - 'id', 'reference', 'container_type__label', 'container_type_id' - ).order_by('container_type__label', 'reference') - - DIVISION_TEMPLATE = """ - - {container} {ref}""" - for ref in q.all(): - if ref['reference'] == old_ref and \ - ref["container_type__label"] == ct: - continue - old_ref = ref['reference'] - ct = ref["container_type__label"] - cpath = current_path[:] - lbl = DIVISION_TEMPLATE.format( - id=ref["id"], container=ref["container_type__label"], - ref=ref['reference']) - cpath.append((ref["id"], lbl)) - remaining_division = list( - ContainerType.objects.filter( - containers__parent__reference=ref['reference'], - containers__parent__container_type_id=ref[ - "container_type_id"], - containers__location=self).distinct()) - for r in self._get_divisions(cpath, remaining_division[:], - depth + 1): - res.append(r) - return res - - @property - def available_division_tuples(self): - """ - :return: ordered list of available paths. Each path is a list of - tuple with the container type and the full reference. - """ - top_divisions = list( - ContainerType.objects.filter( - containers__parent=None, - containers__location=self, - stationary=True).distinct()) - divisions = self._get_divisions([], top_divisions) - return divisions - - def _number_of_items_by_place(self, model, division_key, count_filter=None): - res = {} - paths = self.available_division_tuples[:] - for path in paths: - cpath = [] - for container_id, lbl in path: - cpath.append((container_id, lbl)) - if tuple(cpath) in res: - continue - q = model.objects - for idx, p in enumerate(reversed(cpath)): - container_id, __ = p - div_key = division_key + "parent__" * idx - attrs = { - div_key + "id": container_id - } - q = q.filter(**attrs) - if count_filter: - q = q.filter(**{count_filter: None}) - res[tuple(cpath)] = q.distinct().count() - res = [(k, res[k]) for k in res] - final_res, current_res, depth = [], [], 1 - - len_divisions = self.max_division_number - for path, nb in sorted(res, key=lambda x: (len(x[0]), x[0])): - if len(path) > len_divisions: - continue - if depth != len(path): - final_res.append(current_res[:]) - current_res = [] - depth = len(path) - if path[-1] == '-': - continue - path = [k[1] for k in path] - path = path + ['' for __ in range(len_divisions - len(path))] - current_res.append((path, nb)) - final_res.append(current_res[:]) - return final_res - - def _number_of_finds_by_place(self): - from archaeological_finds.models import Find - return self._number_of_items_by_place( - Find, division_key='inside_container__container__') - - @property - def number_of_finds_by_place(self, update=False): - return self._get_or_set_stats('_number_of_finds_by_place', update) - - def _number_of_containers_by_place(self): - return self._number_of_items_by_place( - ContainerTree, 'container_parent__', 'container__children') - - @property - def number_of_containers_by_place(self, update=False): - return self._get_or_set_stats('_number_of_containers_by_place', update) - def merge(self, item, keep_old=False): # do not recreate missing divisions available_divisions = [ @@ -569,7 +601,7 @@ class ContainerTree(models.Model): class Container(DocumentItem, Merge, LightHistorizedItem, QRCodeItem, GeoItem, - OwnPerms, MainItem): + OwnPerms, MainItem, DivisionContainer): SLUG = 'container' APP = "archaeological-warehouse" MODEL = "container" @@ -617,6 +649,10 @@ class Container(DocumentItem, Merge, LightHistorizedItem, QRCodeItem, GeoItem, 'finds__base_finds__context_record', 'finds': 'finds', 'container_type__label': 'container_type__label', + + # dynamic tables + 'container_tree_child__container_parent__id': + 'container_tree_child__container_parent__id' } COL_LABELS = { 'cached_location': _("Location - index"), @@ -788,6 +824,8 @@ class Container(DocumentItem, Merge, LightHistorizedItem, QRCodeItem, GeoItem, ) QUICK_ACTIONS = [QA_EDIT, QA_LOCK] + BASE_QUERY_LOCATION = "container_tree_child__container_parent" + objects = UUIDModelManager() # fields @@ -852,6 +890,35 @@ class Container(DocumentItem, Merge, LightHistorizedItem, QRCodeItem, GeoItem, def __str__(self): return self.cached_label or "" + @property + def start_division_number(self): + depth = 1 + parent = self.parent + parents = [] + while parent and parent.pk not in parents: + parents.append(parent.pk) + depth += 1 + parent = parent.parent + return depth + + def get_max_division_number(self): + return self.location.get_max_division_number() \ + - self.start_division_number + + @property + def number_of_finds_hosted(self): + count = super(Container, self).number_of_finds_hosted + Find = apps.get_model("archaeological_finds", "Find") + count += Find.objects.filter(container_id=self.pk).count() + return count + + @property + def number_of_finds(self): + count = super(Container, self).number_of_finds + Find = apps.get_model("archaeological_finds", "Find") + count += Find.objects.filter(container_ref_id=self.pk).count() + return count + @property def name(self): return "{} - {}".format(self.container_type.name, self.reference) diff --git a/archaeological_warehouse/templates/ishtar/sheet_container.html b/archaeological_warehouse/templates/ishtar/sheet_container.html index 5116820f0..336684bc8 100644 --- a/archaeological_warehouse/templates/ishtar/sheet_container.html +++ b/archaeological_warehouse/templates/ishtar/sheet_container.html @@ -27,6 +27,13 @@ {% trans "Content" %} + {% endif %} @@ -69,6 +76,22 @@ +
+
{% trans "Number of containers" %}
+
{{item.number_containers}}
+
+
+
{% trans "Number of divisions" %}
+
{{item.number_divisions}}
+
+
+
{% trans "Number of finds" %}
+
{{item.number_of_finds_hosted}}
+
+
+
{% trans "Number of owned finds" %}
+
{{item.number_of_finds}}
+
{% include "ishtar/blocks/sheet_creation_section.html" %} {% field_flex "Old reference" item.old_reference %} {% field_flex_full "Comment" item.comment "
" "
" %} @@ -107,24 +130,110 @@ {% if item.container_content.count or item.children.count %} {% if item.children.count %} - {% trans "Containers" as container_lbl %} - {% dynamic_table_document container_lbl 'containers' 'parent' item.pk 'TABLE_COLS' output 'large' %} +

{% trans "Divisions" %}

+ {% dynamic_table_document '' 'divisions' 'container_tree_child__container_parent__id' item.pk 'TABLE_COLS' output %} +

{% trans "Containers" %}

+ {% dynamic_table_document '' 'non-divisions' 'container_tree_child__container_parent__id' item.pk 'TABLE_COLS' output %} {% endif %} {% if item.container_content.count %} - {% trans "Finds" as finds_lbl %} - {% dynamic_table_document finds_lbl 'finds_inside_container' 'container' item.pk 'TABLE_COLS' output 'large' %} +

{% trans "Finds" %}

+ {% dynamic_table_document '' 'finds_inside_container' 'container' item.pk 'TABLE_COLS' output 'large' %} {% endif %} {% else %}
- {% trans "Empty" %} +  {% trans "Empty" %} +
+ {% endif %} + + + +
+

{% trans "Statistics" %}

+ + {% trans "These numbers are updated hourly" %} + + {% if not item.number_containers and not item.number_divisions %} +
+ +  {% trans "No container/division inside this container." %}
+ + {% else %} +

{% trans "Finds" %}

+ +

{% trans "Finds by location" %}

+ {% for items in item.number_of_finds_by_place %} + {% if items %} + + + {% for location_type in item.division_labels %} + {% endfor %} + + + + + {% for item in items %} + + {% for local in item.0 %}{% endfor %} + + + {% endfor %} + +
{{location_type|title}}{% trans "Total" %}
{{local|safe}}{{item.1}}
{% endif %} + {% endfor %} + +

{% trans "Containers" %}

+ {% if item.number_of_containers_by_place %} +

{% trans "Containers by location in the warehouse" %}

+ {% for items in item.number_of_containers_by_place %} + {% if items %} + + + {% for location_type in item.division_labels %} + {% endfor %} + + + + + {% for item in items %} + + {% for local in item.0 %}{% endfor %} + + + {% endfor %} + +
{{location_type|title}}{% trans "Total" %}
{{local|safe}}{{item.1}}
+ {% endif %} + {% endfor %} + {% endif %} + {% endif %}
+ + {% endblock %} diff --git a/archaeological_warehouse/templates/ishtar/sheet_warehouse.html b/archaeological_warehouse/templates/ishtar/sheet_warehouse.html index 8e143aae9..e6717f230 100644 --- a/archaeological_warehouse/templates/ishtar/sheet_warehouse.html +++ b/archaeological_warehouse/templates/ishtar/sheet_warehouse.html @@ -61,7 +61,7 @@
-
{% trans "Number of container" %}
+
{% trans "Number of containers" %}
{{item.number_containers}}
@@ -70,11 +70,11 @@
{% trans "Number of finds" %}
-
{{item.number_finds}}
+
{{item.number_of_finds_hosted}}
{% trans "Number of owned finds" %}
-
{{item.number_owned_finds}}
+
{{item.number_of_finds}}
{% field_flex_detail "Person in charge" item.person_in_charge %} {% field_flex_detail "Organization" item.organization %} @@ -138,15 +138,15 @@

{% trans "Statistics" %}

{% trans "These numbers are updated hourly" %} -

{% trans "Finds" %}

-
- {% trans "Number of attached finds" as number_of_attached_finds_label %} - {% field_flex_2 number_of_attached_finds_label item.number_of_finds %} - {% trans "Number of hosted finds" as number_of_hosted_finds_label %} - {% field_flex_2 number_of_hosted_finds_label item.number_of_finds_hosted %} + {% if not item.number_containers and not item.number_divisions %} +
+ +  {% trans "No container/division inside this container" %}
- {% if item.number_of_finds_by_place %} + {% else %} + +

{% trans "Finds" %}

{% trans "Finds by location in the warehouse" %}

{% for items in item.number_of_finds_by_place %} {% if items %} @@ -169,15 +169,9 @@ {% endif %} {% endfor %} - {% endif %}

{% trans "Containers" %}

-
- {% trans "Number of containers" as number_of_containers_label %} - {% field_flex_2 number_of_containers_label item.number_of_containers %} -
- {% if item.number_of_containers_by_place %}

{% trans "Containers by location in the warehouse" %}

{% for items in item.number_of_containers_by_place %} {% if items %} @@ -193,13 +187,14 @@ {% for item in items %} {% for local in item.0 %}{{local|safe}}{% endfor %} - {{item.1}} + {{item.1}} {% endfor %} {% endif %} {% endfor %} + {% endif %}
@@ -213,7 +208,10 @@ $(document).ready( function () { if (datatables_i18n) datatable_options['language'] = datatables_i18n; $('.datatables').each( function(){ - $("#" + $(this).attr('id')).DataTable(datatable_options); + var dt_id = "#" + $(this).attr('id'); + if (! $.fn.DataTable.isDataTable(dt_id) ) { + $(dt_id).DataTable(datatable_options); + } }); } ); -- cgit v1.2.3