From d3d84ed7cc5ff3cb9c164838c78cfcee73fbc44e Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Mon, 5 Sep 2016 17:05:03 +0200 Subject: Operation: display statistics on the sheet (refs #2989) --- archaeological_files/models.py | 1 + archaeological_operations/models.py | 198 +++++++++++++++++- .../templates/ishtar/sheet_operation.html | 230 ++++++++++++--------- ishtar_common/models.py | 1 + ishtar_common/static/media/style.css | 4 + ishtar_common/templatetags/link_to_window.py | 2 + ishtar_common/utils.py | 5 + version.py | 2 +- 8 files changed, 348 insertions(+), 95 deletions(-) diff --git a/archaeological_files/models.py b/archaeological_files/models.py index 98223d7ff..eaa9d832e 100644 --- a/archaeological_files/models.py +++ b/archaeological_files/models.py @@ -73,6 +73,7 @@ class File(ClosedItem, BaseHistorizedItem, OwnPerms, ValueGetter, ShortMenuItem, DashboardFormItem): TABLE_COLS = ['numeric_reference', 'year', 'internal_reference', 'file_type', 'saisine_type', 'towns', ] + SHOW_URL = 'show-file' year = models.IntegerField(_(u"Year"), default=lambda: datetime.datetime.now().year) numeric_reference = models.IntegerField( diff --git a/archaeological_operations/models.py b/archaeological_operations/models.py index 2db101104..ccafe5202 100644 --- a/archaeological_operations/models.py +++ b/archaeological_operations/models.py @@ -22,17 +22,18 @@ from itertools import groupby from django.conf import settings from django.contrib.gis.db import models +from django.core.cache import cache from django.core.urlresolvers import reverse from django.db.models import Q, Count, Sum, Max, Avg from django.db.models.signals import post_save, m2m_changed, post_delete from django.forms import ValidationError from django.utils.translation import ugettext_lazy as _, ugettext -from ishtar_common.utils import cached_label_changed +from ishtar_common.utils import cached_label_changed, get_cache, mode from ishtar_common.models import GeneralType, BaseHistorizedItem, \ HistoricalRecords, LightHistorizedItem, OwnPerms, Department, Source,\ - Person, Organization, Town, Dashboard, IshtarUser, ValueGetter, \ + SourceType, Person, Organization, Town, Dashboard, IshtarUser, ValueGetter,\ DocumentTemplate, ShortMenuItem, DashboardFormItem, GeneralRelationType,\ GeneralRecordRelations, post_delete_record_relation, OperationType, \ get_external_id @@ -459,6 +460,199 @@ class Operation(ClosedItem, BaseHistorizedItem, OwnPerms, ValueGetter, self.operation_code = self.get_available_operation_code(self.year) return super(Operation, self).save(*args, **kwargs) + @property + def nb_parcels(self): + nb = 0 + if self.associated_file: + nb = self.associated_file.parcels.count() + if not nb: + nb = self.parcels.count() + return nb + + def _get_or_set_stats(self, funcname, update): + key, val = get_cache(self.__class__, [funcname, self.pk]) + if not update and val is not None: + return val + val = getattr(self, funcname)() + cache.set(key, val, settings.CACHE_TIMEOUT) + return val + + @property + def nb_acts(self, update=False): + _(u"Number of administrative acts") + return self._get_or_set_stats('_nb_acts', update) + + def _nb_acts(self): + return self.administrative_act.count() + + @property + def nb_indexed_acts(self, update=False): + _(u"Number of indexed administrative acts") + return self._get_or_set_stats('_nb_indexed_acts', update) + + def _nb_indexed_acts(self): + return self.administrative_act.filter(act_type__indexed=True).count() + + @property + def nb_context_records(self, update=False): + _(u"Number of context records") + return self._get_or_set_stats('_nb_context_records', update) + + def _nb_context_records(self): + return self.context_record.count() + + @property + def nb_context_records_by_type(self, update=False): + return self._get_or_set_stats('_nb_context_records_by_type', update) + + def _nb_context_records_by_type(self): + nbs = [] + q = self.context_record.values( + 'unit', 'unit__label').distinct().order_by('label') + for res in q.all(): + nbs.append((unicode(res['unit__label']), + self.context_record.filter(unit=res['unit']).count())) + return nbs + + @property + def nb_context_records_by_periods(self, update=False): + return self._get_or_set_stats('_nb_context_records_by_periods', update) + + def _nb_context_records_by_periods(self): + nbs = [] + q = self.context_record.values( + 'datings__period', 'datings__period__label').distinct().order_by( + 'datings__period__order') + for res in q.all(): + nbs.append((unicode(res['datings__period__label']), + self.context_record.filter( + datings__period=res['datings__period']).count())) + return nbs + + @property + def nb_finds(self, update=False): + _(u"Number of finds") + return self._get_or_set_stats('_nb_finds', update) + + def _nb_finds(self): + from archaeological_finds.models import Find + q = Find.objects.filter( + base_finds__context_record__operation=self) + return q.count() + + @property + def nb_finds_by_material_type(self, update=False): + return self._get_or_set_stats('_nb_finds_by_material_type', update) + + def _nb_finds_by_material_type(self): + from archaeological_finds.models import Find + nbs = [] + q = Find.objects.filter( + base_finds__context_record__operation=self).values( + 'material_types__pk', 'material_types__label').distinct().order_by( + 'material_types__label') + for res in q.all(): + nbs.append( + (unicode(res['material_types__label']), + Find.objects.filter( + material_types__pk=res['material_types__pk']).count())) + return nbs + + @property + def nb_finds_by_types(self, update=False): + return self._get_or_set_stats('_nb_finds_by_types', update) + + def _nb_finds_by_types(self): + from archaeological_finds.models import Find + nbs = [] + q = Find.objects.filter( + base_finds__context_record__operation=self).values( + 'object_types', 'object_types__label').distinct().order_by( + 'object_types__label') + for res in q.all(): + label = unicode(res['object_types__label']) + if label == 'None': + label = _(u"No type") + nbs.append( + (label, + Find.objects.filter( + object_types=res['object_types']).count())) + return nbs + + @property + def nb_finds_by_periods(self, update=False): + return self._get_or_set_stats('_nb_finds_by_periods', update) + + def _nb_finds_by_periods(self): + from archaeological_finds.models import Find + nbs = [] + q = Find.objects.filter( + base_finds__context_record__operation=self).values( + 'datings__period', 'datings__period__label').distinct().order_by( + 'datings__period__order') + for res in q.all(): + nbs.append( + (unicode(res['datings__period__label']), + Find.objects.filter( + datings__period=res['datings__period']).count())) + return nbs + + @property + def nb_documents(self, update=False): + _(u"Number of sources") + return self._get_or_set_stats('_nb_documents', update) + + def _nb_documents(self): + from archaeological_context_records.models import ContextRecordSource + from archaeological_finds.models import FindSource + nbs = self.source.count() + \ + ContextRecordSource.objects.filter( + context_record__operation=self).count() + \ + FindSource.objects.filter( + find__base_finds__context_record__operation=self).count() + return nbs + + @property + def nb_documents_by_types(self, update=False): + return self._get_or_set_stats('_nb_documents_by_types', update) + + def _nb_documents_by_types(self): + from archaeological_context_records.models import ContextRecordSource + from archaeological_finds.models import FindSource + docs = {} + + qs = [ + self.source, + ContextRecordSource.objects.filter(context_record__operation=self), + FindSource.objects.filter( + find__base_finds__context_record__operation=self)] + for q in qs: + for res in q.values('source_type').distinct(): + st = res['source_type'] + if st not in docs: + docs[st] = 0 + docs[st] += q.filter(source_type=st).count() + docs = [(unicode(SourceType.objects.get(pk=k)), docs[k]) for k in docs] + return sorted(docs, key=lambda x: x[0]) + + @property + def nb_stats_finds_by_ue(self, update=False): + return self._get_or_set_stats('_nb_stats_finds_by_ue', update) + + def _nb_stats_finds_by_ue(self): + _(u"Mean") + res, finds = {}, [] + for cr in self.context_record.all(): + finds.append(cr.base_finds.count()) + if not finds: + return res + res['mean'] = float(sum(finds)) / max(len(finds), 1) + res['min'] = min(finds) + res['max'] = max(finds) + res['mode'] = u" ; ".join([str(m) for m in mode(finds)]) + return res + + m2m_changed.connect(cached_label_changed, sender=Operation.towns.through) diff --git a/archaeological_operations/templates/ishtar/sheet_operation.html b/archaeological_operations/templates/ishtar/sheet_operation.html index f0846f11f..f8421c830 100644 --- a/archaeological_operations/templates/ishtar/sheet_operation.html +++ b/archaeological_operations/templates/ishtar/sheet_operation.html @@ -6,110 +6,69 @@ {% block content %} {% window_nav item window_id 'show-operation' 'operation_modify' 'show-historized-operation' 'revert-operation' previous next %} -{% if previous or next %} -
-{%if previous%} -{%trans "Previous version"%} ({{previous}}) -{% endif %} -{% if previous and next %} - {% endif %} -{%if next%} -Rollback - -{%trans "Next version"%} ({{next}}) -{% endif %} -
-{% endif %} - -
{%trans "Export as:"%} {%trans "OpenOffice.org file"%}, {%trans "PDF file"%}
- -
{% trans "Modify" %}
- {% if item.virtual_operation %}

{% trans "This operation is virtual." %}

{% endif %} - -

{% trans "General"%}

-{% if item.common_name %}

{{ item.common_name }}

{% endif %} -{% if item.year %}

{{ item.year }}

{% endif %} -{% if item.operation_code %}

{{ item.operation_code }}

{% endif %} - -{% if item.code_patriarche %}

OA{{ item.code_patriarche }}

{%else%} -

{%trans "Patriarche OA code not yet recorded!"%}

{%endif%} -{% field "Old code" item.old_code %} - -

{% if item.history_date %}{{ item.history_date }}{% else %}{{ item.history.all.0.history_date }}{% endif %}

-

{{ item.history_creator.ishtaruser.full_label }}

- -{% if item.start_date %}

{{ item.start_date }}

-

{{ item.excavation_end_date|default:"-" }}

-{%endif%} -{% if item.scientist %}

{{ item.scientist.full_label }}

{%endif%} -{% if item.in_charge %}

{{ item.in_charge.full_label }}

{%endif%} -{% if item.operator %}

{{ item.operator }}

{% endif %} -

{% if item.is_active %}{%trans "Active file"%}

-{% else %}{%trans "Closed operation"%}

-{% if item.closing.date %}

{{ item.closing.date }} {%trans "by" %} {{ item.closing.user }}

{% endif %} -{% endif %} -{% field "Report delivery date" item.report_delivery_date %} -{% field "Report processing" item.report_processing %} -

{{ item.operation_type }}

-{% if item.surface %}

{{ item.surface }} m2 ({{ item.surface_ha }} ha)

{% endif %} -{% if item.cost %}

{{ item.cost }} €{% if item.cost_by_m2 %}, ({{ item.cost_by_m2 }} €/m2){%endif%}

{%endif%} -{% if item.duration %}

{{ item.duration }} {%trans "Day"%}s

{%endif%} - -{% field_multiple "Remains" item.remains %} -{% field_multiple "Periods" item.periods %} - -{% if item.QUALITY_DICT %} -{% field "Record quality" item.record_quality|from_dict:item.QUALITY_DICT %} +{% if not item.code_patriarche %} +

{%trans "Patriarche OA code not yet recorded!"%}

{% endif %} -{% if item.history_object and item.history_object.QUALITY_DICT %} -{% field "Record quality" item.record_quality|from_dict:item.history_object.QUALITY_DICT %} -{% endif %} - -{% field "Abstract" item.abstract %} -{% if item.associated_file %} -

{{ item.associated_file }}

-{% if item.associated_file.is_preventive %} -{#{% if item.operator_reference_code %}

{{ item.operator_reference_code }}

{% endif %}#} - -{% field "Responsible for town planning service" item.associated_file.responsible_town_planning_service.full_address %} +

{% trans "General"%}

+ {% if item.towns.count %}

{% trans "Localisation"%}

-

{{ item.towns.all|join:", " }}

+ {% endif %} -{% if item.associated_file.address %}

{{ item.associated_file.address }}

-{% if item.associated_file.address_complement %}

{{ item.associated_file.address_complement }}

{%endif%} -{% if item.associated_file.postal_code %}

{{ item.associated_file.postal_code }}

{%endif%} -{%endif%} -{% comment %} -

{{ item.lambert_x }}

-

{{ item.lambert_y }}

-

{{ item.altitude }}

-{% endcomment %} - {% if item.right_relations.count %}

{% trans "Relations"%}

{% for rel in item.right_relations.all %} @@ -130,8 +89,8 @@ {% include "ishtar/blocks/window_tables/parcels.html" %} {% if item.administrative_act %} -{% trans "Administrative acts" as administrativeacts_label %} -{% table_administrativact administrativeacts_label item.administrative_act.all %} +

{% trans "Administrative acts" %}

+{% table_administrativact "" item.administrative_act.all %} {% endif %} {% trans "Document from this operation" as operation_docs %} @@ -139,9 +98,9 @@ {% dynamic_table_document operation_docs 'operation_docs' 'operation' item.pk '' output %} {% endif %} -{% trans "Context records" as context_records %} {% if item.context_record.count %} -{% dynamic_table_document context_records 'context_records_for_ope' 'operation' item.pk 'TABLE_COLS_FOR_OPE' output %} +

{% trans "Context records" %}

+{% dynamic_table_document '' 'context_records_for_ope' 'operation' item.pk 'TABLE_COLS_FOR_OPE' output %} {% endif %} {% trans "Documents from associated context records" as cr_docs %} @@ -159,4 +118,91 @@ {% dynamic_table_document finds_docs 'finds_docs' 'find__base_finds__context_record__operation' item.pk '' output %} {% endif %} +

{% trans "Statistics" %}

+ +

{% trans "Administrative acts" %}

+ + +

{% trans "Context records" %}

+ + + +

{% trans "Finds" %}

+ + + +

{% trans "Sources" %}

+ + + +{% if item.nb_stats_finds_by_ue %} +

{% trans "Finds by context records" %}

+ +{% endif %} + {% endblock %} diff --git a/ishtar_common/models.py b/ishtar_common/models.py index c536b64ae..186d88f1a 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -2200,6 +2200,7 @@ pre_delete.connect(pre_delete_import, sender=Import) class Organization(Address, Merge, OwnPerms, ValueGetter): TABLE_COLS = ('name', 'organization_type',) + SHOW_URL = 'show-organization' name = models.CharField(_(u"Name"), max_length=500) organization_type = models.ForeignKey(OrganizationType, verbose_name=_(u"Type")) diff --git a/ishtar_common/static/media/style.css b/ishtar_common/static/media/style.css index 02f4d930b..7dd424974 100644 --- a/ishtar_common/static/media/style.css +++ b/ishtar_common/static/media/style.css @@ -1276,6 +1276,10 @@ table.table-form td input{ border-color:#922; } +.clean-table.small { + width: auto; +} + .clean-table { margin: 10px 0 10px 0; width: 100%; diff --git a/ishtar_common/templatetags/link_to_window.py b/ishtar_common/templatetags/link_to_window.py index 288bfcd8a..93924b77a 100644 --- a/ishtar_common/templatetags/link_to_window.py +++ b/ishtar_common/templatetags/link_to_window.py @@ -10,6 +10,8 @@ register = Library() @register.filter def link_to_window(item): + if not item: + return "" return mark_safe( u' ' diff --git a/ishtar_common/utils.py b/ishtar_common/utils.py index 9fe7a3a00..d4973012e 100644 --- a/ishtar_common/utils.py +++ b/ishtar_common/utils.py @@ -56,3 +56,8 @@ def shortify(lbl, number=20): if len(lbl) <= number: return lbl return lbl[:number - len(SHORTIFY_STR)] + SHORTIFY_STR + + +def mode(array): + most = max(list(map(array.count, array))) + return list(set(filter(lambda x: array.count(x) == most, array))) diff --git a/version.py b/version.py index 0f4077755..424b4478e 100644 --- a/version.py +++ b/version.py @@ -1,4 +1,4 @@ -VERSION = (0, 97, 4) +VERSION = (0, 97, 4, 1) def get_version(): -- cgit v1.2.3