diff options
| -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> | 
