diff options
| -rw-r--r-- | ishtar/ishtar_base/menus.py | 3 | ||||
| -rw-r--r-- | ishtar/ishtar_base/models.py | 165 | ||||
| -rw-r--r-- | ishtar/ishtar_base/views.py | 13 | ||||
| -rw-r--r-- | ishtar/templates/dashboard_main.html | 47 | ||||
| -rw-r--r-- | static/media/style.css | 52 | 
5 files changed, 274 insertions, 6 deletions
| diff --git a/ishtar/ishtar_base/menus.py b/ishtar/ishtar_base/menus.py index db80efec0..8061495f1 100644 --- a/ishtar/ishtar_base/menus.py +++ b/ishtar/ishtar_base/menus.py @@ -283,6 +283,9 @@ class Menu:          #    ]),          SectionItem('dashboard', _(u"Dashboard"),              childs=[ +                    MenuItem('dashboard_main', _(u"General informations"), +                        model=models.File, +                        access_controls=['change_file',]),                      MenuItem('dashboard_file', _(u"Files"),                          model=models.File,                          access_controls=['change_file',]), diff --git a/ishtar/ishtar_base/models.py b/ishtar/ishtar_base/models.py index 2a8248804..cbf362b83 100644 --- a/ishtar/ishtar_base/models.py +++ b/ishtar/ishtar_base/models.py @@ -27,7 +27,7 @@ from django.core.validators import validate_slug  from django.utils.translation import ugettext_lazy as _, ugettext  from django.db.utils import DatabaseError  from django.utils.safestring import SafeUnicode, mark_safe -from django.db.models import Q, Max +from django.db.models import Q, Max, Count  from django.db.models.signals import m2m_changed  from django.contrib.auth.models import User @@ -348,6 +348,77 @@ class LightHistorizedItem(BaseHistorizedItem):          super(LightHistorizedItem, self).save(*args, **kwargs)          return True +class Dashboard: +    def __init__(self, model): +        self.model = model +        self.total_number = model.get_total_number() +        # years +        self.years = model.get_years() +        self.years.sort() +        if not self.total_number or not self.years: +            return +        self.values = [('year', _(u"Year"), reversed(self.years))] +        # numbers +        self.numbers = [model.get_by_year(year).count() for year in self.years] +        self.values += [('number', _(u"Number"), reversed(self.numbers))] +        # calculate +        self.average = self.get_average() +        self.variance = self.get_variance() +        self.standard_deviation = self.get_standard_deviation() +        self.median = self.get_median() +        self.mode = self.get_mode() +        # by operation +        if not hasattr(model, 'get_by_operation'): +            return +        operations = model.get_operations() +        operation_numbers = [model.get_by_operation(op).count() +                             for op in operations] +        # calculate +        self.operation_average = self.get_average(operation_numbers) +        self.operation_variance = self.get_variance(operation_numbers) +        self.operation_standard_deviation = self.get_standard_deviation( +                                                       operation_numbers) +        self.operation_median = self.get_median(operation_numbers) +        operation_mode_pk = self.get_mode(dict(zip(operations, +                                                     operation_numbers))) +        if operation_mode_pk: +            self.operation_mode = unicode(Operation.objects.get( +                                                       pk=operation_mode_pk)) + +    def get_average(self, vals=[]): +        if not vals: +            vals = self.numbers +        return sum(vals)/len(vals) + +    def get_variance(self, vals=[]): +        if not vals: +            vals = self.numbers +        avrg = self.get_average(vals) +        return self.get_average([(x-avrg)**2 for x in vals]) + +    def get_standard_deviation(self, vals=[]): +        if not vals: +            vals = self.numbers +        return round(self.get_variance(vals)**0.5, 3) + +    def get_median(self, vals=[]): +        if not vals: +            vals = self.numbers +        len_vals = len(vals) +        vals.sort() +        if (len_vals % 2) == 1: +            return vals[len_vals/2] +        else: +            return (vals[len_vals/2-1] + vals[len_vals/2])/2.0 + +    def get_mode(self, vals={}): +        if not vals: +            vals = dict(zip(self.years, self.numbers)) +        mx = max(vals.values()) +        for v in vals: +            if vals[v] == mx: +                return v +  class Departement(models.Model):      label = models.CharField(_(u"Label"), max_length=30)      number = models.CharField(_(u"Number"), unique=True, max_length=3) @@ -571,6 +642,19 @@ class File(BaseHistorizedItem, OwnPerms):          )          ordering = ['-year', '-numeric_reference'] +    @classmethod +    def get_years(cls): +        return [res['year'] for res in list(cls.objects.values('year').annotate( +                                             Count("id")).order_by())] + +    @classmethod +    def get_by_year(cls, year): +        return cls.objects.filter(year=year) + +    @classmethod +    def get_total_number(cls): +        return cls.objects.count() +      def __unicode__(self):          items = [unicode(_('Intercommunal'))]          if self.towns.count() == 1: @@ -686,6 +770,19 @@ class Operation(BaseHistorizedItem, OwnPerms):                                 unicode(self.operation_code))))          return JOINT.join(items) +    @classmethod +    def get_years(cls): +        return [res['year'] for res in list(cls.objects.values('year').annotate( +                                             Count("id")).order_by())] + +    @classmethod +    def get_by_year(cls, year): +        return cls.objects.filter(year=year) + +    @classmethod +    def get_total_number(cls): +        return cls.objects.count() +      year_index_lbl = _(u"Operation code")      @property      def year_index(self): @@ -930,6 +1027,32 @@ class ContextRecord(BaseHistorizedItem, OwnPerms):          return JOINT.join([unicode(lbl) for lbl in [self.parcel.operation.year,                                             self.parcel.operation.operation_code,                                             self.label] if lbl]) + +    @classmethod +    def get_years(cls): +        years = set() +        for res in list(cls.objects.values('operation__start_date')): +            yr = res['operation__start_date'].year +            years.add(yr) +        return list(years) + +    @classmethod +    def get_by_year(cls, year): +        return cls.objects.filter(operation__start_date__year=year) + +    @classmethod +    def get_operations(cls): +        return [dct['operation__pk'] +                for dct in cls.objects.values('operation__pk').distinct()] + +    @classmethod +    def get_by_operation(cls, operation_id): +        return cls.objects.filter(operation__pk=operation_id) + +    @classmethod +    def get_total_number(cls): +        return cls.objects.filter(operation__start_date__isnull=False).count() +  class ContextRecordSource(Source):      class Meta:          verbose_name = _(u"Context record documentation") @@ -1036,6 +1159,46 @@ class Item(BaseHistorizedItem, OwnPerms):                                    blank=True, null=True, related_name='items')      history = HistoricalRecords() +    @classmethod +    def get_years(cls): +        years = set() +        items = cls.objects.filter(downstream_treatment__isnull=True) +        for item in items: +            bi = item.base_items.all() +            if not bi: +                continue +            bi = bi[0] +            yr = bi.context_record.operation.start_date.year +            years.add(yr) +        return list(years) + +    @classmethod +    def get_by_year(cls, year): +        return cls.objects.filter(downstream_treatment__isnull=True, +                base_items__context_record__operation__start_date__year=year) + +    @classmethod +    def get_operations(cls): +        operations = set() +        items = cls.objects.filter(downstream_treatment__isnull=True) +        for item in items: +            bi = item.base_items.all() +            if not bi: +                continue +            bi = bi[0] +            pk = bi.context_record.operation.pk +            operations.add(pk) +        return list(operations) + +    @classmethod +    def get_by_operation(cls, operation_id): +        return cls.objects.filter(downstream_treatment__isnull=True, +                base_items__context_record__operation__pk=operation_id) + +    @classmethod +    def get_total_number(cls): +        return cls.objects.filter(downstream_treatment__isnull=True).count() +      def duplicate(self, user):          dct = dict([(attr, getattr(self, attr)) for attr in ('order', 'label',                          'description', 'material_type', 'volume', 'weight', diff --git a/ishtar/ishtar_base/views.py b/ishtar/ishtar_base/views.py index 631e71ed7..ff55169c5 100644 --- a/ishtar/ishtar_base/views.py +++ b/ishtar/ishtar_base/views.py @@ -649,3 +649,16 @@ def action(request, action_slug, obj_id=None, *args, **kwargs):      return render_to_response('index.html', dct,                                context_instance=RequestContext(request)) +def dashboard_main(request, dct, obj_id=None, *args, **kwargs): +    """ +    Main dashboard +    """ +    dct = {'items':[ +        (_(u"Archaeological files"), models.Dashboard(models.File)), +        (_(u"Operations"), models.Dashboard(models.Operation)), +        (_(u"Context records"), models.Dashboard(models.ContextRecord)), +        (_(u"Archaeological items"), models.Dashboard(models.Item)), +        ]} +    return render_to_response('dashboard_main.html', dct, +                              context_instance=RequestContext(request)) + diff --git a/ishtar/templates/dashboard_main.html b/ishtar/templates/dashboard_main.html new file mode 100644 index 000000000..ae243f7c2 --- /dev/null +++ b/ishtar/templates/dashboard_main.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} +{% load i18n %} +{% load range %} +{% block extra_head %} +{{form.media}} +{% endblock %} +{% block content %} +<div class='dashboard'> +{% for lbl, dashboard in items %} +  <div> +    <h3>{{lbl}}</h3> +    <h4>{% trans "Numbers" %}</h4> +    <p><strong>{% trans "Total:" %}</strong> {{dashboard.total_number}}</p> +    <div class='table'> +    <table> +    {% for idx, lbl, values in dashboard.values %} +      <tr class='idx {% if forloop.counter0|divisibleby:"2" %}even{%else%}odd{%endif%}'> +      <th>{{lbl}}</th> +      {% for value in values %}<td>{{value}}</td>{% endfor%} +      </tr> +    {% endfor%} +    </table> +    </div> +    {% if dashboard.years %} +    <h4>{% trans "By years" %}</h4> +    <ul> +      <li><strong>{% trans "Average:" %}</strong> {{dashboard.average}}</li> +      <li><strong>{% trans "Variance:" %}</strong> {{dashboard.variance}}</li> +      <li><strong>{% trans "Standard deviation:" %}</strong> {{dashboard.standard_deviation}}</li> +      <li><strong>{% trans "Median:" %}</strong> {{dashboard.median}}</li> +      <li><strong>{% trans "Mode:" %}</strong> {{dashboard.mode}}</li> +    </ul> +    {% endif %} +    {% if dashboard.operation_average %} +    <h4>{% trans "By operations" %}</h4> +    <ul> +      <li><strong>{% trans "Average:" %}</strong> {{dashboard.operation_average}}</li> +      <li><strong>{% trans "Variance:" %}</strong> {{dashboard.operation_variance}}</li> +      <li><strong>{% trans "Standard deviation:" %}</strong> {{dashboard.operation_standard_deviation}}</li> +      <li><strong>{% trans "Median:" %}</strong> {{dashboard.operation_median}}</li> +      <li><strong>{% trans "Mode:" %}</strong> {{dashboard.operation_mode}}</li> +    </ul> +    {% endif %} +  </div> +{% endfor%} +</div> +{% endblock %} diff --git a/static/media/style.css b/static/media/style.css index 2d8cb7ff7..a6cd7487e 100644 --- a/static/media/style.css +++ b/static/media/style.css @@ -321,30 +321,64 @@ table.confirm tr.spacer td:last-child{      border-radius:8px;  } -#window .table{ +.dashboard > div{ +    width:760px; +    background: #FFF; +    -moz-border-radius:8px; +    -webkit-border-radius:8px; +    border-radius:8px; +    -webkit-box-shadow: 0px 0px 20px #444; +    -moz-box-shadow: 0px 0px 20px #444; +    margin:20px; +    text-align:left; +    padding-bottom:10px; +} + +.dashboard h3{ +    background-color:#922; +    color:#FFF; +    -webkit-border-top-left-radius: 8px; +    -webkit-border-top-right-radius: 8px; +    -moz-border-radius-topleft: 8px; +    -moz-border-radius-topright: 8px; +    border-top-left-radius: 8px; +    border-top-right-radius: 8px; +} + +.dashboard h4{ +    font-weight:normal; +    color:#D14; +} +.dashboard h4, .dashboard p{ +    margin:0; +    padding:0 10px; +} + +#window .table, .dashboard .table{ +    padding:10px;      width:730px;      overflow:auto;  } -#window table{ +#window table, .dashboard table{      font-size:0.9em;      margin:10px 0;      border-collapse:collapse;      width:100%;  } -#window caption{ +#window caption, .dashboard caption{      font-size:1.2em;  } -#window table th{ +#window table th, .dashboard table th{      text-align:center;      background-color:#922;      border:1px solid #EEE;      color:#FFF;  } -#window table td{ +#window table td, .dashboard table td{      text-align:right;      padding:0 1em;      border:1px solid #EEE; @@ -453,3 +487,11 @@ a.remove{      margin:0 6px;  } +.dashboard table{ +    width:100%; +    border-collapse:yes; +} +.dashboard table th, .dashboard table td{ +    border:1px solid; +} + | 
