diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2020-09-30 17:56:39 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2021-02-28 12:15:20 +0100 |
commit | e24fb4b832e80a52d1ed28d870a2d63eca2ef6c1 (patch) | |
tree | 54ae0fb12f10f5106ccef9562a3235b3ab76e477 | |
parent | 596414d4c90355de00a2f45293c12653e40b7718 (diff) | |
download | Ishtar-e24fb4b832e80a52d1ed28d870a2d63eca2ef6c1.tar.bz2 Ishtar-e24fb4b832e80a52d1ed28d870a2d63eca2ef6c1.zip |
Sheet container: show statistics
-rw-r--r-- | archaeological_warehouse/models.py | 409 | ||||
-rw-r--r-- | archaeological_warehouse/templates/ishtar/sheet_container.html | 119 | ||||
-rw-r--r-- | archaeological_warehouse/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 = """<a class="display_details" + href="#" onclick="load_window('/show-container/{id}/')"> + <i class="fa fa-info-circle" aria-hidden="true"></i> + </a> {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" @@ -156,28 +351,6 @@ class Warehouse(Address, DocumentItem, GeoItem, QRCodeItem, DashboardFormItem, 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 = """<a class="display_details" - href="#" onclick="load_window('/show-container/{id}/')"> - <i class="fa fa-info-circle" aria-hidden="true"></i></a> - {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 @@ -853,6 +891,35 @@ class Container(DocumentItem, Merge, LightHistorizedItem, QRCodeItem, GeoItem, 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" %} </a> </li> + <li class="nav-item"> + <a class="nav-link" id="{{window_id}}-stats-tab" + data-toggle="tab" href="#{{window_id}}-stats" role="tab" + aria-controls="{{window_id}}-stats" aria-selected="false"> + {% trans "Statistics" %} + </a> + </li> </ul> {% endif %} @@ -69,6 +76,22 @@ </nav> </dd> </dl> + <dl class="col-6 col-md-3 flex-wrap"> + <dt>{% trans "Number of containers" %}</dt> + <dd>{{item.number_containers}}</dd> + </dl> + <dl class="col-6 col-md-3 flex-wrap"> + <dt>{% trans "Number of divisions" %}</dt> + <dd>{{item.number_divisions}}</dd> + </dl> + <dl class="col-6 col-md-3 flex-wrap"> + <dt>{% trans "Number of finds" %}</dt> + <dd>{{item.number_of_finds_hosted}}</dd> + </dl> + <dl class="col-6 col-md-3 flex-wrap"> + <dt>{% trans "Number of owned finds" %}</dt> + <dd>{{item.number_of_finds}}</dd> + </dl> {% include "ishtar/blocks/sheet_creation_section.html" %} {% field_flex "Old reference" item.old_reference %} {% field_flex_full "Comment" item.comment "<pre>" "</pre>" %} @@ -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' %} + <h4>{% trans "Divisions" %}</h4> + {% dynamic_table_document '' 'divisions' 'container_tree_child__container_parent__id' item.pk 'TABLE_COLS' output %} + <h4>{% trans "Containers" %}</h4> + {% 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' %} + <h4>{% trans "Finds" %}</h4> + {% dynamic_table_document '' 'finds_inside_container' 'container' item.pk 'TABLE_COLS' output 'large' %} {% endif %} {% else %} <div class="alert alert-info"> <i class="fa fa-exclamation-triangle"></i> - <em>{% trans "Empty" %}</em> + <em>{% trans "Empty" %}</em> + </div> + {% endif %} + + </div> + + <div class="tab-pane fade" id="{{window_id}}-stats" + role="tabpanel" aria-labelledby="{{window_id}}-stats-tab"> + <h3>{% trans "Statistics" %}</h3> + + <small class="centered"><em>{% trans "These numbers are updated hourly" %}</em></small> + + {% if not item.number_containers and not item.number_divisions %} + <div class="alert alert-info"> + <i class="fa fa-exclamation-triangle"></i> + <em>{% trans "No container/division inside this container." %}</em> </div> + + {% else %} + <h4>{% trans "Finds" %}</h4> + + <h4>{% trans "Finds by location" %}</h4> + {% for items in item.number_of_finds_by_place %} + {% if items %} + <table class='table table-striped datatables' + id="{{window_id}}-find-by-loca-{{forloop.counter}}"> + <thead> + <tr>{% for location_type in item.division_labels %} + <th class="text-center">{{location_type|title}}</th>{% endfor %} + <th class="text-center">{% trans "Total" %}</th> + </tr> + </thead> + <tbody> + {% for item in items %} + <tr> + {% for local in item.0 %}<td>{{local|safe}}</td>{% endfor %} + <td class="text-right">{{item.1}}</td> + </tr> + {% endfor %} + </tbody> + </table> {% endif %} + {% endfor %} + + <h4>{% trans "Containers" %}</h4> + {% if item.number_of_containers_by_place %} + <h4>{% trans "Containers by location in the warehouse" %}</h4> + {% for items in item.number_of_containers_by_place %} + {% if items %} + <table class='table table-striped datatables' + id="{{window_id}}-container-by-loca-{{forloop.counter}}"> + <thead> + <tr>{% for location_type in item.division_labels %} + <th class="text-center">{{location_type|title}}</th>{% endfor %} + <th class="text-center">{% trans "Total" %}</th> + </tr> + </thead> + <tbody> + {% for item in items %} + <tr> + {% for local in item.0 %}<td>{{local|safe}}</td>{% endfor %} + <td class="text-right">{{item.1}}</td> + </tr> + {% endfor %} + </tbody> + </table> + {% endif %} + {% endfor %} + {% endif %} + {% endif %} </div> </div> +<script type="text/javascript"> +$(document).ready( function () { + datatable_options = { + "dom": 'ltip', + }; + $.extend(datatable_options, datatables_static_default); + if (datatables_i18n) datatable_options['language'] = datatables_i18n; + $('.datatables').each( + function(){ + var dt_id = "#" + $(this).attr('id'); + if (! $.fn.DataTable.isDataTable(dt_id) ) { + $(dt_id).DataTable(datatable_options); + } + }); +} ); +</script> + {% 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 @@ <div class='row'> <dl class="col-6 col-md-3 flex-wrap"> - <dt>{% trans "Number of container" %}</dt> + <dt>{% trans "Number of containers" %}</dt> <dd>{{item.number_containers}}</dd> </dl> <dl class="col-6 col-md-3 flex-wrap"> @@ -70,11 +70,11 @@ </dl> <dl class="col-6 col-md-3 flex-wrap"> <dt>{% trans "Number of finds" %}</dt> - <dd>{{item.number_finds}}</dd> + <dd>{{item.number_of_finds_hosted}}</dd> </dl> <dl class="col-6 col-md-3 flex-wrap"> <dt>{% trans "Number of owned finds" %}</dt> - <dd>{{item.number_owned_finds}}</dd> + <dd>{{item.number_of_finds}}</dd> </dl> {% field_flex_detail "Person in charge" item.person_in_charge %} {% field_flex_detail "Organization" item.organization %} @@ -138,15 +138,15 @@ <h3>{% trans "Statistics" %}</h3> <small class="centered"><em>{% trans "These numbers are updated hourly" %}</em></small> - <h4>{% trans "Finds" %}</h4> - <div class='row'> - {% 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 %} + <div class="alert alert-info"> + <i class="fa fa-exclamation-triangle"></i> + <em>{% trans "No container/division inside this container" %}</em> </div> - {% if item.number_of_finds_by_place %} + {% else %} + + <h4>{% trans "Finds" %}</h4> <h4>{% trans "Finds by location in the warehouse" %}</h4> {% for items in item.number_of_finds_by_place %} {% if items %} @@ -169,15 +169,9 @@ </table> {% endif %} {% endfor %} - {% endif %} <h4>{% trans "Containers" %}</h4> - <div class='row'> - {% trans "Number of containers" as number_of_containers_label %} - {% field_flex_2 number_of_containers_label item.number_of_containers %} - </div> - {% if item.number_of_containers_by_place %} <h4>{% trans "Containers by location in the warehouse" %}</h4> {% for items in item.number_of_containers_by_place %} {% if items %} @@ -193,13 +187,14 @@ {% for item in items %} <tr> {% for local in item.0 %}<td>{{local|safe}}</td>{% endfor %} - <td class="text-center">{{item.1}}</td> + <td class="text-right">{{item.1}}</td> </tr> {% endfor %} </tbody> </table> {% endif %} {% endfor %} + {% endif %} </div> </div> @@ -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); + } }); } ); </script> |