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; +} + |