diff --git a/.gitignore b/.gitignore index 4996b376b436862040d84b8cd8b7aa059d67db9f..e0934d5bb8aa607ffdb9744cea0969892cf32c5c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,6 @@ local_settings.py #tmp files appointment-import/testFiles/~* appointment-import/tmp.sql -.idea *.iml out +.idea diff --git a/smash/web/forms.py b/smash/web/forms.py index 514d9ac9f2a080c3908e756236e404a9b591e5eb..a461805b1a2f5c4ebe7e7e7676943971a784986c 100644 --- a/smash/web/forms.py +++ b/smash/web/forms.py @@ -2,6 +2,7 @@ from datetime import datetime from django import forms from django.forms import ModelForm, Form +from django.utils.dates import MONTHS from models import Subject, Worker, Appointment, Visit @@ -21,6 +22,7 @@ DATETIMEPICKER_DATE_ATTRS = { 'class': 'datetimepicker', 'data-date-format': 'Y-MM-DD HH:mm', } +START_YEAR_STATISTICS = 2015 def validate_subject_nd_number(self): @@ -221,3 +223,23 @@ class KitRequestForm(Form): widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d"), required=False ) + + +class StatisticsForm(Form): + def __init__(self, *args, **kwargs): + super(StatisticsForm, self).__init__(*args) + visit_choices = kwargs['visit_choices'] + month = kwargs['month'] + year = kwargs['year'] + now = datetime.now() + year_now = now.year + number_of_years_for_statistics = year_now - START_YEAR_STATISTICS + 2 + + year_choices = [(START_YEAR_STATISTICS + i, START_YEAR_STATISTICS + i) for i in + range(0, number_of_years_for_statistics + 1)] + self.fields['month'] = forms.ChoiceField(choices=MONTHS.items(), initial=month) + self.fields['year'] = forms.ChoiceField(choices=year_choices, initial=year) + choices = [(-1, "all")] + choices.extend(Subject.SUBJECT_TYPE_CHOICES.items()) + self.fields['subject_type'] = forms.ChoiceField(choices=choices, initial="-1") + self.fields['visit'] = forms.ChoiceField(choices=visit_choices, initial="-1") diff --git a/smash/web/models.py b/smash/web/models.py index 03f784fc8399a99ea3c8f92339eff5cbf43bdafe..56811396e49cb1b461671646cd84a363813e1913 100644 --- a/smash/web/models.py +++ b/smash/web/models.py @@ -50,10 +50,10 @@ class Subject(models.Model): ) SUBJECT_TYPE_CHOICES_CONTROL = 'C' - SUBJECT_TYPE_CHOICES = ( - (SUBJECT_TYPE_CHOICES_CONTROL, 'CONTROL'), - ('P', 'PATIENT'), - ) + SUBJECT_TYPE_CHOICES = { + SUBJECT_TYPE_CHOICES_CONTROL: 'CONTROL', + 'P': 'PATIENT', + } def finish_all_visits(self): visits = Visit.objects.filter(subject=self, is_finished=False) @@ -95,7 +95,7 @@ class Subject(models.Model): verbose_name='Contact on', ) type = models.CharField(max_length=1, - choices=SUBJECT_TYPE_CHOICES, + choices=SUBJECT_TYPE_CHOICES.items(), verbose_name='Type' ) @@ -545,12 +545,12 @@ class Appointment(models.Model): APPOINTMENT_STATUS_FINISHED = 'FINISHED' APPOINTMENT_STATUS_CANCELLED = 'CANCELLED' APPOINTMENT_STATUS_NO_SHOW = 'NO_SHOW' - APPOINTMENT_STATUS_CHOICES = ( - (APPOINTMENT_STATUS_SCHEDULED, 'Scheduled'), - (APPOINTMENT_STATUS_FINISHED, 'Finished'), - (APPOINTMENT_STATUS_CANCELLED, 'Cancelled'), - (APPOINTMENT_STATUS_NO_SHOW, 'No Show'), - ) + APPOINTMENT_STATUS_CHOICES = { + APPOINTMENT_STATUS_SCHEDULED: 'Scheduled', + APPOINTMENT_STATUS_FINISHED: 'Finished', + APPOINTMENT_STATUS_CANCELLED: 'Cancelled', + APPOINTMENT_STATUS_NO_SHOW: 'No Show', + } flying_team = models.ForeignKey(FlyingTeam, verbose_name='Flying team (if applicable)', @@ -588,7 +588,7 @@ class Appointment(models.Model): verbose_name='Appointment length (in minutes)' ) # Potentially redundant; but can be used to manually adjust appointment's length - status = models.CharField(max_length=20, choices=APPOINTMENT_STATUS_CHOICES, + status = models.CharField(max_length=20, choices=APPOINTMENT_STATUS_CHOICES.items(), verbose_name='Status', editable=False, default=APPOINTMENT_STATUS_SCHEDULED diff --git a/smash/web/statistics.py b/smash/web/statistics.py new file mode 100644 index 0000000000000000000000000000000000000000..0de80c539f380d7c016101fd7520a09c1ac4b8d8 --- /dev/null +++ b/smash/web/statistics.py @@ -0,0 +1,212 @@ +# coding=utf-8 +import copy +import datetime +from collections import defaultdict +from operator import attrgetter + +from django.db import connection +from django.db.models import Q, Count + +from .models import AppointmentType, Appointment, Visit + +__author__ = 'Valentin Grouès' + +QUERY_VISITS_RANKS = """ +SELECT DISTINCT(rank() OVER (PARTITION BY subject_id ORDER BY datetime_begin)) AS rank FROM web_visit +""" + +QUERY_APPOINTMENTS_COUNT = """ +SELECT count(*) FROM web_appointment LEFT JOIN (SELECT id, rank() +OVER (PARTITION BY subject_id ORDER BY datetime_begin) AS rnk FROM web_visit) +a ON a.id = web_appointment.visit_id WHERE a.rnk = %s +AND EXTRACT(MONTH FROM web_appointment.datetime_when) = %s +AND EXTRACT(YEAR FROM web_appointment.datetime_when) = %s +""" + +QUERY_VISITS_ENDED_COUNT = """ +SELECT count(*) FROM (SELECT id, datetime_begin, datetime_end, rank() +OVER (PARTITION BY subject_id ORDER BY datetime_begin) AS rnk FROM web_visit) a +WHERE a.rnk = %s AND +EXTRACT(MONTH FROM a.datetime_end) = %s AND +EXTRACT(YEAR FROM a.datetime_end) = %s +""" + +QUERY_VISITS_STARTED_COUNT = """ +SELECT count(*) FROM (SELECT id, datetime_begin, datetime_end, rank() +OVER (PARTITION BY subject_id ORDER BY datetime_begin) AS rnk FROM web_visit) a +WHERE a.rnk = %s AND +EXTRACT(MONTH FROM a.datetime_begin) = %s AND +EXTRACT(YEAR FROM a.datetime_begin) = %s +""" + +QUERY_APPOINTMENTS = """ +SELECT types.appointmenttype_id, web_appointment.status, count(*) FROM web_appointment +LEFT JOIN (SELECT id, rank() OVER (PARTITION BY subject_id ORDER BY datetime_begin) AS rnk +FROM web_visit) a ON a.id = web_appointment.visit_id LEFT JOIN web_appointment_appointment_types types +ON types.appointment_id = web_appointment.id WHERE a.rnk = %s +AND EXTRACT(MONTH FROM web_appointment.datetime_when) = %s +AND EXTRACT(YEAR FROM web_appointment.datetime_when) = %s +GROUP BY types.appointmenttype_id, web_appointment.status +""" + + +class StatisticsManager(object): + def __init__(self): + self.appointment_types = {appointment_type.id: appointment_type for appointment_type in + AppointmentType.objects.all()} + self.visits_ranks = self._get_visits_ranks() + self.statuses_list = Appointment.objects.filter().values_list('status', flat=True).distinct().order_by( + 'status').all() + self.statuses_labels = [Appointment.APPOINTMENT_STATUS_CHOICES.get(status, status.title()) for status in + self.statuses_list] + + def get_statistics_for_month(self, month, year, subject_type=None, visit=None): + """ + Build dict with statistics for a given month of a given year. + Statistics include: + - number of appointments, + - number of visits ended, + - number of visits started + - number of appointements per type and per status + :param month: the month number [1;12] + :type month: int + :param year: the year (4 digits) + :type year: int + :param subject_type: the type of subject (patient or control or None for all) + :type subject_type: basestring + :param visit: the visit number or None for all + :type visit: basestring + :return: a dictionary containing the statistics + :rtype: dict + """ + results = {} + general_results = {} + + filters_month_year_appointments, filters_month_year_visits_ended, filters_month_year_visits_started = self._build_filters( + month, subject_type, year) + + number_of_appointments = self._get_number_of_appointments(filters_month_year_appointments, visit, month, year) + number_of_visits_started = self._get_number_visits_started(filters_month_year_visits_started, visit, month, + year) + number_of_visits_ended = self._get_number_visits_ended(filters_month_year_visits_ended, visit, month, year) + + general_results["appointments"] = number_of_appointments + general_results["visits_started"] = number_of_visits_started + general_results["visits_ended"] = number_of_visits_ended + + results["general"] = general_results + + results_appointments = self.get_appointments_per_type_and_status(filters_month_year_appointments, month, + self.statuses_list, visit, year) + results["appointments"] = results_appointments + results["statuses_list"] = self.statuses_labels + appointment_types_values = map(attrgetter('code'), self.appointment_types.values()) + sorted_appointment_types_values = sorted(appointment_types_values) + results["appointments_types_list"] = sorted_appointment_types_values + return results + + def get_appointments_per_type_and_status(self, filters_month_year_appointments, month, statuses_list, visit, year): + if not visit: + results_appointments = {} + for appointment_type in self.appointment_types.values(): + appointment_type_filters = copy.deepcopy(filters_month_year_appointments) + appointment_type_filters.add(Q(appointment_types=appointment_type), Q.AND) + results_appointment_set = Appointment.objects.filter(appointment_type_filters).values( + 'status').order_by( + 'status').annotate( + Count('status')) + results_appointment = [Appointment.objects.filter(appointment_type_filters).count()] + results_appointment_per_status = {result['status']: result['status__count'] for result in + results_appointment_set} + + results_appointment.extend([results_appointment_per_status.get(status, 0) for status in statuses_list]) + results_appointments[appointment_type.code] = results_appointment + else: + results_appointment_set = defaultdict(dict) + with connection.cursor() as cursor: + cursor.execute(QUERY_APPOINTMENTS, [visit, month, year]) + rows = cursor.fetchall() + for row in rows: + appointment_type_id, status, count = row + results_appointment_set[appointment_type_id][status] = int(count) + results_appointments = {} + for appointment_type in self.appointment_types.values(): + if appointment_type.id not in results_appointment_set: + results_appointments[appointment_type.code] = [0 * i for i in range(0, len(statuses_list) + 1)] + continue + results_appointment_set_for_type = results_appointment_set[appointment_type.id] + total = [sum(results_appointment_set_for_type.values())] + total.extend([results_appointment_set_for_type.get(status, 0) for status in statuses_list]) + results_appointments[appointment_type.code] = total + return results_appointments + + @staticmethod + def _get_count_from_filters_or_sql(model, filters, query, visit, month, year): + if visit: + with connection.cursor() as cursor: + cursor.execute( + query, + [visit, month, year]) + row = cursor.fetchone() + count = int(row[0]) + else: + count = model.objects.filter(filters).count() + return count + + def _get_number_visits_started(self, filters_month_year_visits_started, visit, month, year): + return self._get_count_from_filters_or_sql(Visit, filters_month_year_visits_started, QUERY_VISITS_STARTED_COUNT, + visit, month, year) + + def _get_number_visits_ended(self, filters_month_year_visits_ended, visit, month, year): + return self._get_count_from_filters_or_sql(Visit, filters_month_year_visits_ended, QUERY_VISITS_ENDED_COUNT, + visit, month, year) + + def _get_number_of_appointments(self, filters, visit, month, year): + return self._get_count_from_filters_or_sql(Appointment, filters, QUERY_APPOINTMENTS_COUNT, visit, month, year) + + @staticmethod + def _build_filters(month, subject_type, year): + filters_month_year_appointments = Q() + filters_month_year_appointments.add(Q(datetime_when__month=month), Q.AND) + filters_month_year_appointments.add(Q(datetime_when__year=year), Q.AND) + + filters_month_year_visits_started = Q() + filters_month_year_visits_started.add(Q(datetime_begin__month=month), Q.AND) + filters_month_year_visits_started.add(Q(datetime_begin__year=year), Q.AND) + + filters_month_year_visits_ended = Q() + filters_month_year_visits_ended.add(Q(datetime_end__month=month), Q.AND) + filters_month_year_visits_ended.add(Q(datetime_end__year=year), Q.AND) + + if subject_type is not None: + subject_type_filter = Q(subject__type=subject_type) + filters_month_year_visits_started.add(subject_type_filter, Q.AND) + filters_month_year_visits_ended.add(subject_type_filter, Q.AND) + subject_type_filter_appointements = Q(visit__subject__type=subject_type) + filters_month_year_appointments.add(subject_type_filter_appointements, Q.AND) + return filters_month_year_appointments, filters_month_year_visits_ended, filters_month_year_visits_started + + @staticmethod + def _get_visits_ranks(): + with connection.cursor() as cursor: + cursor.execute( + QUERY_VISITS_RANKS, + []) + rows = cursor.fetchall() + + return [r[0] for r in rows] + + +def get_previous_year_and_month(): + now = datetime.datetime.now() + return get_previous_year_and_month_for_date(now) + + +def get_previous_year_and_month_for_date(now): + previous_month = now.month - 1 or 12 + year_now = now.year + if previous_month == 12: + year_previous_month = year_now - 1 + else: + year_previous_month = year_now + return year_previous_month, previous_month diff --git a/smash/web/templates/_base.html b/smash/web/templates/_base.html index 685769f28a16609d7ec2f345e91e5ecc64703178..feceaeed1ec4f054a4670257224e0743880e5dfa 100644 --- a/smash/web/templates/_base.html +++ b/smash/web/templates/_base.html @@ -218,106 +218,7 @@ desired effect <!-- sidebar: style can be found in sidebar.less --> <section class="sidebar"> - - {% comment "Uncomment if needed" %} - <!-- Sidebar user panel (optional) --> - <div class="user-panel"> - <div class="pull-left image"> - <!-- - <img src="dist/img/user2-160x160.jpg" class="img-circle" alt="User Image"> - --> - <i class="fa fa-user-circle-o fa-3x white" style="color: rgba(255,255,255,0.8)"></i> - </div> - <div class="pull-left info"> - <p>Prenom Nom</p> - <!-- Status --> - <a href="#"><i class="fa fa-circle text-success"></i> Online</a> - </div> - </div> - {% endcomment %} - - {% comment "Uncomment if necessary' %} - <!-- search form (Optional) --> - <form action="#" method="get" class="sidebar-form"> - <div class="input-group"> - <input type="text" name="q" class="form-control" placeholder="Search..."> - <span class="input-group-btn"> - <button type="submit" name="search" id="search-btn" class="btn btn-flat"><i class="fa fa-search"></i> - </button> - </span> - </div> - </form> - <!-- /.search form --> - {% endcomment %} - - <!-- Sidebar Menu --> - <ul class="sidebar-menu"> - <li class="header">Pages</li> - - <li data-desc="subjects"> - <a href="{% url 'web.views.subjects' %}"> - <i class="fa fa-users"></i> - <span>Subjects</span> - </a> - </li> - - <li data-desc="visits"> - <a href="{% url 'web.views.visits' %}"> - <i class="fa fa-id-card-o"></i> - <span>Visits</span> - </a> - </li> - - <li data-desc="appointments"> - <a href="{% url 'web.views.appointments' %}"> - <i class="fa fa-calendar"></i> - <span>Appointments</span> - </a> - </li> - - <li data-desc="workers"> - <a href="{% url 'web.views.doctors' %}"> - <i class="fa fa-user-md"></i> - <span>Workers</span> - </a> - </li> - - <li data-desc="equipment_and_rooms"> - <a href="{% url 'web.views.equipment_and_rooms' %}"> - <i class="fa fa-building-o"></i> - <span>Equipment&rooms</span> - </a> - </li> - - <li data-desc="mail_templates"> - <a href="{% url 'web.views.mail_templates' %}"> - <i class="fa fa-envelope-o"></i> - <span>Mail templates</span> - </a> - </li> - - <li data-desc="export"> - <a href="{% url 'web.views.export' %}"> - <i class="fa fa-file-excel-o"></i> - <span>Export</span> - </a> - </li> - - {% comment "Multi-level" %} - <li class="treeview"> - <a href="#"><i class="fa fa-link"></i> <span>Multilevel</span> - <span class="pull-right-container"> - <i class="fa fa-angle-left pull-right"></i> - </span> - </a> - <ul class="treeview-menu"> - <li><a href="#">Link in level 2</a></li> - <li><a href="#">Link in level 2</a></li> - </ul> - </li> - {% endcomment %} - </ul> - <!-- /.sidebar-menu --> + {% include "sidebar.html" %} </section> <!-- /.sidebar --> </aside> diff --git a/smash/web/templates/sidebar.html b/smash/web/templates/sidebar.html new file mode 100644 index 0000000000000000000000000000000000000000..7dbbeb016be68b380ca0b7516eea1f9b56dea4c3 --- /dev/null +++ b/smash/web/templates/sidebar.html @@ -0,0 +1,61 @@ +<!-- Sidebar Menu --> +<ul class="sidebar-menu"> + <li class="header">Pages</li> + + <li data-desc="subjects"> + <a href="{% url 'web.views.subjects' %}"> + <i class="fa fa-users"></i> + <span>Subjects</span> + </a> + </li> + + <li data-desc="visits"> + <a href="{% url 'web.views.visits' %}"> + <i class="fa fa-id-card-o"></i> + <span>Visits</span> + </a> + </li> + + <li data-desc="appointments"> + <a href="{% url 'web.views.appointments' %}"> + <i class="fa fa-calendar"></i> + <span>Appointments</span> + </a> + </li> + + <li data-desc="workers"> + <a href="{% url 'web.views.doctors' %}"> + <i class="fa fa-user-md"></i> + <span>Workers</span> + </a> + </li> + + <li data-desc="equipment_and_rooms"> + <a href="{% url 'web.views.equipment_and_rooms' %}"> + <i class="fa fa-building-o"></i> + <span>Equipment&rooms</span> + </a> + </li> + + <li data-desc="statistics"> + <a href="{% url 'web.views.statistics' %}"> + <i class="fa fa-bar-chart" aria-hidden="true"></i> + <span>Statistics</span> + </a> + </li> + + <li data-desc="mail_templates"> + <a href="{% url 'web.views.mail_templates' %}"> + <i class="fa fa-envelope-o"></i> + <span>Mail templates</span> + </a> + </li> + + <li data-desc="export"> + <a href="{% url 'web.views.export' %}"> + <i class="fa fa-file-excel-o"></i> + <span>Export</span> + </a> + </li> + +</ul> \ No newline at end of file diff --git a/smash/web/templates/statistics/breadcrumb.html b/smash/web/templates/statistics/breadcrumb.html new file mode 100644 index 0000000000000000000000000000000000000000..2470896f37cfd00aec1456cbff23b37c441a7e8d --- /dev/null +++ b/smash/web/templates/statistics/breadcrumb.html @@ -0,0 +1,2 @@ +<li><a href="{% url 'web.views.appointments' %}"><i class="fa fa-dashboard"></i> Dashboard</a></li> +<li class="active"><a href="{% url 'web.views.statistics' %}">Statistics</a></li> \ No newline at end of file diff --git a/smash/web/templates/statistics/index.html b/smash/web/templates/statistics/index.html new file mode 100644 index 0000000000000000000000000000000000000000..15c67f4ff7cead7d673008993d1f38bd9761443f --- /dev/null +++ b/smash/web/templates/statistics/index.html @@ -0,0 +1,110 @@ +{% extends "_base.html" %} +{% load static %} +{% load filters %} + +{% block ui_active_tab %}'statistics'{% endblock ui_active_tab %} +{% block page_header %}Monthly statistics{% endblock page_header %} +{% block page_description %}{% endblock page_description %} + +{% block breadcrumb %} + {% include "mail_templates/breadcrumb.html" %} +{% endblock breadcrumb %} + +{% block styles %} + {{ block.super }} + <style type="text/css"> + #appointments-table { + table-layout: fixed !important; + } + + #appointments-table tr td { + width: 80px; + text-align: center; + } + + #appointments-table tr td:first-child { + text-align: left; + } + + #appointments-table thead tr td { + background-color: #eee; + } + + #appointments-table tbody tr td:first-child { + background-color: #eee; + } + + #form-month { + margin-bottom: 50px; + } + </style> +{% endblock %} + + +{% block maincontent %} + + <div class="row"> + <div class="col-md-8"> + <form id="form-month" method="get" class="form-inline"> + {# {% csrf_token %}#} + {% for field in form %} + <div class="form-group"> + <label class="control-label">{{ field.label }}</label> + {{ field | add_class:'form-control' }} + </div> + {% endfor %} + </form> + </div> + </div> + <div class="row"> + {% include 'statistics/small_box.html' with value=monthly_statistics.general.appointments label="Appointments" color="aqua" icon="calendar" %} + {% include 'statistics/small_box.html' with value=monthly_statistics.general.visits_started label="Visits started" color="green" icon="hourglass-start" %} + {% include 'statistics/small_box.html' with value=monthly_statistics.general.visits_ended label="Visits ended" color="red" icon="hourglass-end" %} + </div> + <div class="row"> + <div class="col-lg-9 col-md-12"> + <div class="box"> + <div class="box-header with-border"> + <h3 class="box-title"><i class="fa fa-calendar"></i> Appointments</h3> + </div> + <div class="box-body"> + <table id="appointments-table" class="table table-bordered table-striped table-hover"> + <thead> + <tr> + <td>Type \ Status</td> + <td>Total</td> + {% for status in monthly_statistics.statuses_list %} + <td>{{ status }}</td> + {% endfor %} + </tr> + </thead> + <tbody> + {% for appointment_type in monthly_statistics.appointments_types_list %} + <tr> + <td>{{ appointment_type }}</td> + {{ monthly_statistics.appointments | render_appointments:appointment_type }} + </tr> + {% endfor %} + </tbody> + </table> + </div> + <div class="box-footer clearfix"> + </div> + </div> + </div> + </div> + +{% endblock maincontent %} + +{% block scripts %} + {{ block.super }} + + + <script> + $(document).ready(function () { + $('#form-month select').change(function () { + $('#form-month').submit(); + }); + }); + </script> +{% endblock scripts %} diff --git a/smash/web/templates/statistics/small_box.html b/smash/web/templates/statistics/small_box.html new file mode 100644 index 0000000000000000000000000000000000000000..e5b6e1f91a3dea620452471e990b1eed3caf882c --- /dev/null +++ b/smash/web/templates/statistics/small_box.html @@ -0,0 +1,11 @@ +<div class="col-md-4 col-lg-3"> + <!-- small box --> + <div class="info-box "> + <span class="info-box-icon bg-{{ color }}"><i class="fa fa-{{ icon }}"></i></span> + + <div class="info-box-content"> + <span class="info-box-text">{{ label }}</span> + <span class="info-box-number">{{ value }}</span> + </div> + </div> +</div> \ No newline at end of file diff --git a/smash/web/templatetags/filters.py b/smash/web/templatetags/filters.py index 2df497f0ddaaefa14b4fcc94294fdee4913176b1..45f03ee0f9e2a18ca2eb368001e24a63d4c7b75a 100644 --- a/smash/web/templatetags/filters.py +++ b/smash/web/templatetags/filters.py @@ -1,5 +1,6 @@ # See: http://stackoverflow.com/a/18962481 from django import template +from django.utils.safestring import mark_safe register = template.Library() @@ -21,3 +22,11 @@ def add_class(value, arg): def disable(value): value.field.widget.attrs['disabled'] = 'disabled' return value + + +@register.filter(name="render_appointments") +def render_appointments(statistics, appointment_type): + html = "" + for status_count in statistics.get(appointment_type, []): + html += '<td>{}</td>'.format(status_count) + return mark_safe(html) diff --git a/smash/web/tests/functions.py b/smash/web/tests/functions.py index e5b5f0e43d30486f76c00fad1bbf31832bac1d88..728bf683282c458aa9d22b482ae3334244880fa4 100644 --- a/smash/web/tests/functions.py +++ b/smash/web/tests/functions.py @@ -31,7 +31,8 @@ def create_subject(): first_name="Piotr", last_name="Gawron", default_location=get_test_location(), - sex=Subject.SEX_CHOICES_MALE) + sex=Subject.SEX_CHOICES_MALE, + type=Subject.SUBJECT_TYPE_CHOICES_CONTROL) def create_user(): @@ -62,10 +63,11 @@ def create_visit(subject=None): is_finished=False) -def create_appointment(visit=None): +def create_appointment(visit=None, when=None): if visit is None: visit = create_visit() return Appointment.objects.create( visit=visit, length=30, - location=get_test_location()) + location=get_test_location(), + datetime_when=when) diff --git a/smash/web/tests/test_statistics.py b/smash/web/tests/test_statistics.py new file mode 100644 index 0000000000000000000000000000000000000000..3c6286cebb821a52293a041401f062737dfdade1 --- /dev/null +++ b/smash/web/tests/test_statistics.py @@ -0,0 +1,83 @@ +# coding=utf-8 +import datetime + +from django.test import TestCase + +from web.models import Visit +from web.statistics import get_previous_year_and_month_for_date, StatisticsManager +from web.tests.functions import create_appointment, create_appointment_type + +__author__ = 'Valentin Grouès' + + +class TestStatistics(TestCase): + def setUp(self): + self.now = datetime.datetime.now() + self.appointment_type = create_appointment_type() + appointment = create_appointment(when=self.now) + appointment.appointment_types = [self.appointment_type] + appointment.save() + self.subject = appointment.visit.subject + self.statistics_manager = StatisticsManager() + + def test_get_previous_year_and_month_for_date(self): + test_date = datetime.datetime(year=2014, month=10, day=13) + previous_year, previous_month = get_previous_year_and_month_for_date(test_date) + self.assertEqual(2014, previous_year) + self.assertEqual(9, previous_month) + test_date = datetime.datetime(year=2014, month=1, day=13) + previous_year, previous_month = get_previous_year_and_month_for_date(test_date) + self.assertEqual(2013, previous_year) + self.assertEqual(12, previous_month) + + def test_get_statistics_for_month_one_appointment(self): + statistics = self.statistics_manager.get_statistics_for_month(self.now.month - 1, self.now.year) + self.check_statistics(statistics, 1, 0, 0, {"C": [0, 0]}, ['Scheduled']) + + statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year) + self.check_statistics(statistics, 0, 0, 1, {"C": [1, 1]}, ['Scheduled']) + + statistics = self.statistics_manager.get_statistics_for_month(self.now.month + 1, self.now.year) + self.check_statistics(statistics, 0, 1, 0, {"C": [0, 0]}, ['Scheduled']) + + def test_get_statistics_for_month_one_appointment_visit(self): + statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, visit="1") + self.check_statistics(statistics, 0, 0, 1, {"C": [1, 1]}, ['Scheduled']) + + statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, visit="2") + self.check_statistics(statistics, 0, 0, 0, {"C": [0, 0]}, ['Scheduled']) + + def test_get_statistics_for_month_one_appointment_subject_type(self): + statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, subject_type="C") + self.check_statistics(statistics, 0, 0, 1, {"C": [1, 1]}, ['Scheduled']) + + statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, subject_type="P") + self.check_statistics(statistics, 0, 0, 0, {"C": [0, 0]}, ['Scheduled']) + + def test_get_statistics_for_month_multiple_visits(self): + second_visit = Visit.objects.create(datetime_begin=self.now + datetime.timedelta(days=-32), + datetime_end=self.now + datetime.timedelta(days=31), + subject=self.subject, + is_finished=False) + second_appointment = create_appointment(second_visit, when=self.now) + second_appointment.appointment_types = [self.appointment_type] + second_appointment.status = "Cancelled" + second_appointment.save() + self.statistics_manager = StatisticsManager() + + statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year) + self.check_statistics(statistics, 0, 0, 2, {"C": [2, 1, 1]}, ['Cancelled', 'Scheduled']) + + statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, visit="1") + self.check_statistics(statistics, 0, 0, 1, {"C": [1, 1, 0]}, ['Cancelled', 'Scheduled']) + + statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, visit="2") + self.check_statistics(statistics, 0, 0, 1, {"C": [1, 0, 1]}, ['Cancelled', 'Scheduled']) + + def check_statistics(self, statistics, expected_visits_started, expected_visits_ended, expected_appointments_count, + expected_appointments_details, expected_statuses): + self.assertEqual(expected_visits_started, statistics['general']['visits_started']) + self.assertEqual(expected_visits_ended, statistics['general']['visits_ended']) + self.assertEqual(expected_statuses, statistics['statuses_list']) + self.assertEqual(expected_appointments_count, statistics['general']['appointments']) + self.assertEqual(expected_appointments_details, statistics['appointments']) diff --git a/smash/web/tests/test_view_statistics.py b/smash/web/tests/test_view_statistics.py new file mode 100644 index 0000000000000000000000000000000000000000..b070ca19c432aa222731847af288cb49ffc7c22a --- /dev/null +++ b/smash/web/tests/test_view_statistics.py @@ -0,0 +1,30 @@ +# coding=utf-8 +from datetime import datetime + +from django.contrib.auth.models import User +from django.test import Client +from django.test import TestCase +from django.urls import reverse + +__author__ = 'Valentin Grouès' + + +class TestStatisticsView(TestCase): + def setUp(self): + self.client = Client() + username = 'piotr' + password = 'top_secret' + self.user = User.objects.create_user( + username=username, email='jacob@bla', password=password) + self.client.login(username=username, password=password) + + def test_statistics_request(self): + url = reverse('web.views.statistics') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + current_month = datetime.now().month - 1 or 12 + content = response.content + self.assertIn('<option value="{}" selected="selected">'.format(current_month), content) + response = self.client.get(url, {"month": 10, "year": 2017, "subject_type": -1, "visit": -1}) + content = response.content + self.assertIn('<option value="10" selected="selected">October', content) diff --git a/smash/web/urls.py b/smash/web/urls.py index 4712179e99e4667e9911d93afa9e1d2df3c7ce49..6cbc1fcefde1c3ad82ea887069b1d56693b3dee1 100644 --- a/smash/web/urls.py +++ b/smash/web/urls.py @@ -71,6 +71,7 @@ urlpatterns = [ views.kit_requests_send_mail, name='web.views.kit_requests_send_mail'), url(r'^mail_templates$', views.mail_templates, name='web.views.mail_templates'), + url(r'^statistics$', views.statistics, name='web.views.statistics'), url(r'^export$', views.export, name='web.views.export'), url(r'^export/(?P<type>[A-z]+)$', views.export_to_csv2, name='web.views.export_to_csv2'), diff --git a/smash/web/views.py b/smash/web/views.py index 30ea696fdd5247fc3213c04cd9b3eeef6c38c021..3197798bbc00555ea3cb291dafc256aa17f9707b 100644 --- a/smash/web/views.py +++ b/smash/web/views.py @@ -16,8 +16,11 @@ from django.utils.dateparse import parse_datetime from auth import do_logout, do_login from forms import SubjectDetailForm, WorkerEditForm, WorkerDetailForm, AppointmentDetailForm, AppointmentAddForm, \ - AppointmentEditForm, KitRequestForm, SubjectEditForm, SubjectAddForm, VisitAddForm, WorkerAddForm, VisitDetailForm + AppointmentEditForm, KitRequestForm, SubjectEditForm, SubjectAddForm, VisitAddForm, WorkerAddForm, VisitDetailForm, \ + StatisticsForm from models import Worker, Location, Visit, Subject, Appointment, Avaibility, Item, AppointmentType +from statistics import StatisticsManager +from statistics import get_previous_year_and_month handler404 = 'web.views.e404_page_not_found' handler500 = 'web.views.e500_error' @@ -863,3 +866,26 @@ def kit_requests(request): def kit_requests_send_mail(request, start_date, end_date=None): return wrap_response(request, 'equipment_and_rooms/kit_requests_send_mail.html', get_kit_requests_data(request, start_date, end_date)) + +def statistics(request): + statistics_manager = StatisticsManager() + visit_choices = [("-1", "all")] + visit_choices.extend([(rank, rank) for rank in statistics_manager.visits_ranks]) + year_previous_month, previous_month = get_previous_year_and_month() + + form = StatisticsForm(request.GET, visit_choices=visit_choices, month=previous_month, year=year_previous_month) + if not form.is_valid(): + form.is_bound = False + month = form.data.get('month', previous_month) + year = form.data.get('year', year_previous_month) + subject_type = form.data.get('subject_type', "-1") + visit = form.data.get('visit', "-1") + if subject_type == "-1": + subject_type = None + if visit == "-1": + visit = None + monthly_statistics = statistics_manager.get_statistics_for_month(month, year, subject_type, visit) + return wrap_response(request, 'statistics/index.html', { + 'form': form, + 'monthly_statistics': monthly_statistics + }) \ No newline at end of file