diff --git a/.gitignore b/.gitignore index 5f846bdc44dc42490d2b3c88d19b3dc0f50533e3..9eca034d61d74660f3cb2fe12292148b84f5e15c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ smash/~/ # Disable python bytecode *.pyc +#vim swap files +*.swp +#vim backup files +*~ # Disable local developer settings local_settings.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7cf83b16820ded2b91b06faef75aad4f0645f8c5..8cfd55f3ec6c0d9daf2a9017bd6295d33d66b10d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,4 +19,4 @@ test: - cd smash - python manage.py makemigrations web && python manage.py migrate - coverage run --source web manage.py test - - coverage report -m + - coverage report -m --omit="*/test*,*/migrations*" diff --git a/requirements.txt b/requirements.txt index fa5a7c14be038ab020a18cf2c7e4201737d76b11..67644e195dd3578eaeba9c357afff56d394170f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,8 @@ python-docx==0.8.6 django-cleanup==0.4.2 django_cron==0.5.0 django-two-factor-auth==1.6.1 -nexmo \ No newline at end of file +nexmo +django-excel==0.0.9 +pyexcel-xls==0.5.0 +pyexcel==0.5.3 + diff --git a/smash/web/api_views/appointment.py b/smash/web/api_views/appointment.py index 8945d166e824ceb7e8bf40396273b77194ac04c3..db8122ed6b31d7e0fb2438b0988e1c3af7c9af63 100644 --- a/smash/web/api_views/appointment.py +++ b/smash/web/api_views/appointment.py @@ -1,8 +1,10 @@ import traceback +from datetime import datetime from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.urls import reverse +from django.utils import timezone from web.models import Appointment from web.views import e500_error @@ -29,9 +31,11 @@ def get_appointments(request, type, min_date, max_date): raise TypeError("Unknown query type: " + type) if min_date is not None: + min_date = datetime.strptime(min_date, "%Y-%m-%d").replace(tzinfo=timezone.now().tzinfo) result = result.filter(datetime_when__gt=min_date) if max_date is not None: + max_date = datetime.strptime(max_date, "%Y-%m-%d").replace(tzinfo=timezone.now().tzinfo) result = result.filter(datetime_when__lt=max_date) return result.order_by("datetime_when") @@ -57,12 +61,12 @@ def appointments(request, type): sliced_subjects = all_appointments[start:(start + length)] - appointments = sliced_subjects + result_appointments = sliced_subjects count_filtered = all_appointments.count() data = [] - for appointment in appointments: + for appointment in result_appointments: data.append(serialize_appointment(appointment)) return JsonResponse({ diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py index 8f0161dd65b4aba72e97662896659431a4e0029a..df1aa3535a35b36a152bafdd782964ae0698c911 100644 --- a/smash/web/api_views/subject.py +++ b/smash/web/api_views/subject.py @@ -10,25 +10,25 @@ from web.views.subject import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJE @login_required def cities(request): - subjects = Subject.objects.filter(city__isnull=False).values_list('city').distinct() + result_subjects = Subject.objects.filter(city__isnull=False).values_list('city').distinct() return JsonResponse({ - "cities": [x[0] for x in subjects] + "cities": [x[0] for x in result_subjects] }) @login_required def countries(request): - subjects = Subject.objects.filter(country__isnull=False).values_list('country').distinct() + result_subjects = Subject.objects.filter(country__isnull=False).values_list('country').distinct() return JsonResponse({ - "countries": [x[0] for x in subjects] + "countries": [x[0] for x in result_subjects] }) @login_required def referrals(request): - subjects = Subject.objects.filter(referral__isnull=False).values_list('referral').distinct() + result_subjects = Subject.objects.filter(referral__isnull=False).values_list('referral').distinct() return JsonResponse({ - "referrals": [x[0] for x in subjects] + "referrals": [x[0] for x in result_subjects] }) @@ -135,12 +135,12 @@ def subjects(request, type): filtered_subjects = get_subjects_filtered(ordered_subjects, filters) sliced_subjects = filtered_subjects[start:(start + length)] - subjects = sliced_subjects + result_subjects = sliced_subjects count_filtered = filtered_subjects.count() data = [] - for subject in subjects: + for subject in result_subjects: data.append(serialize_subject(subject)) return JsonResponse({ diff --git a/smash/web/static/css/export.css b/smash/web/static/css/export.css new file mode 100644 index 0000000000000000000000000000000000000000..ffab23233d813aab070b92753890709a998839ed --- /dev/null +++ b/smash/web/static/css/export.css @@ -0,0 +1,9 @@ +.cell { + display: table-cell; + border-bottom: 1px solid black; + border: 1px solid #BBBBBB; + padding:4px; + text-align: center; + vertical-align: middle; + +} diff --git a/smash/web/templates/export/index.html b/smash/web/templates/export/index.html index b1813ec7f47d6d675a61a633580272169aa20250..a4e8aef298bbe802d4997a9f54f325ca03b8d0c5 100644 --- a/smash/web/templates/export/index.html +++ b/smash/web/templates/export/index.html @@ -3,6 +3,7 @@ {% block styles %} {{ block.super }} + <link rel="stylesheet" href="{% static 'css/export.css' %}"> {% endblock styles %} {% block ui_active_tab %}'export'{% endblock ui_active_tab %} @@ -18,15 +19,21 @@ {% block maincontent %} <div> - <a href="{% url 'web.views.export_to_csv2' 'subjects' %}" class="btn btn-app"> - <i class="fa fa-download"></i> - Subjects - </a> - <br/> - <a href="{% url 'web.views.export_to_csv2' 'appointments' %}" class="btn btn-app"> - <i class="fa fa-download"></i> - Appointments - </a> + <h3>Subjects</h3> + <ul> + <li><a href="{% url 'web.views.export_to_excel' 'subjects' %}"><i class="fa fa-file-excel-o"></i> XLS - + Excel</a> + </li> + <li><a href="{% url 'web.views.export_to_excel' 'appointments' %}"><i class="fa fa-file-text-o"></i> CSV - + Text based</a></li> + </ul> + <h3>Appointments</h3> + <ul> + <li><a href="{% url 'web.views.export_to_csv' 'subjects' %}"><i class="fa fa-file-excel-o"></i> XLS - + Excel</a></li> + <li><a href="{% url 'web.views.export_to_csv' 'appointments' %}"><i class="fa fa-file-text-o"></i> CSV - + Text based</a></li> + </ul> </div> <div class="box-body"> diff --git a/smash/web/templates/visits/add.html b/smash/web/templates/visits/add.html index 9e03dbec7d16943bfdcbca5cb129b6f1f25d76d4..749641be57b7d9c1e4160a5d7457fff0cbfa5fed 100644 --- a/smash/web/templates/visits/add.html +++ b/smash/web/templates/visits/add.html @@ -25,12 +25,6 @@ <a href="{% url 'web.views.visits' %}" class="btn btn-block btn-default">Cancel</a> </div> - {% comment %} - <div class="box-header with-border"> - <h3 class="box-title">Details of a visit</h3> - </div> - {% endcomment %} - <form method="post" action="" class="form-horizontal"> {% csrf_token %} diff --git a/smash/web/tests/test_view_appointments.py b/smash/web/tests/test_view_appointments.py index e32a1b54a167a1aeaf9eb19da20b4a8484d91b88..8caa43d8487a125a9956f044d522e9b4f40dbc0f 100644 --- a/smash/web/tests/test_view_appointments.py +++ b/smash/web/tests/test_view_appointments.py @@ -81,10 +81,14 @@ class AppointmentsViewTests(LoggedInTestCase): form_subject = SubjectEditForm(instance=subject, prefix="subject") form_data = {} for key, value in form_appointment.initial.items(): - if value is not None: + if isinstance(value, datetime.datetime): + form_data['appointment-{}'.format(key)] = value.strftime('%Y-%m-%d %H:%M') + elif value is not None: form_data['appointment-{}'.format(key)] = value for key, value in form_subject.initial.items(): - if value is not None: + if isinstance(value, datetime.datetime): + form_data['subject-{}'.format(key)] = value.strftime('%Y-%m-%d %H:%M') + elif value is not None: form_data['subject-{}'.format(key)] = value form_data["appointment-status"] = Appointment.APPOINTMENT_STATUS_FINISHED diff --git a/smash/web/tests/test_view_export.py b/smash/web/tests/test_view_export.py new file mode 100644 index 0000000000000000000000000000000000000000..f15c2cf439a9012999ff91ef88a9fc1e928a0679 --- /dev/null +++ b/smash/web/tests/test_view_export.py @@ -0,0 +1,27 @@ +# coding=utf-8 +from django.urls import reverse + +from functions import create_subject, create_appointment +from . import LoggedInTestCase + + +class TestExportView(LoggedInTestCase): + def test_export_subjects_to_csv(self): + create_subject() + response = self.client.get(reverse('web.views.export_to_csv', kwargs={'data_type': "subjects"})) + self.assertEqual(response.status_code, 200) + + def test_export_appointments_to_csv(self): + create_appointment() + response = self.client.get(reverse('web.views.export_to_csv', kwargs={'data_type': "appointments"})) + self.assertEqual(response.status_code, 200) + + def test_export_subjects_to_excel(self): + create_subject() + response = self.client.get(reverse('web.views.export_to_excel', kwargs={'data_type': "subjects"})) + self.assertEqual(response.status_code, 200) + + def test_export_appointments_to_excel(self): + create_appointment() + response = self.client.get(reverse('web.views.export_to_excel', kwargs={'data_type': "appointments"})) + self.assertEqual(response.status_code, 200) diff --git a/smash/web/urls.py b/smash/web/urls.py index 332b57008700a272e3962fbc9ce23eceffa4bb75..a2e1a0191019f89ae7f1db70d641c63c9e2fe171 100644 --- a/smash/web/urls.py +++ b/smash/web/urls.py @@ -160,7 +160,8 @@ urlpatterns = [ #################### url(r'^export$', views.export.export, name='web.views.export'), - url(r'^export/(?P<type>[A-z]+)$', views.export.export_to_csv2, name='web.views.export_to_csv2'), + url(r'^export/csv/(?P<data_type>[A-z]+)$', views.export.export_to_csv, name='web.views.export_to_csv'), + url(r'^export/xls/(?P<data_type>[A-z]+)$', views.export.export_to_excel, name='web.views.export_to_excel'), #################### # CONFIGURATION # diff --git a/smash/web/views/export.py b/smash/web/views/export.py index 6f98432d6a056091e83a53173d503d0692290f66..359386c2552b4bd0c7e8afd46bb2353beba5917e 100644 --- a/smash/web/views/export.py +++ b/smash/web/views/export.py @@ -1,6 +1,7 @@ # coding=utf-8 import csv +import django_excel as excel from django.contrib.auth.decorators import login_required from django.http import HttpResponse @@ -10,23 +11,43 @@ from ..models import Subject, Appointment @login_required -def export_to_csv2(request, type="subjects"): +def export_to_csv(request, data_type="subjects"): # Create the HttpResponse object with the appropriate CSV header. response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename="' + type + '-' + get_today_midnight_date().strftime( + response['Content-Disposition'] = 'attachment; filename="' + data_type + '-' + get_today_midnight_date().strftime( "%Y-%m-%d") + '.csv"' + if data_type == "subjects": + data = get_subjects_as_array() + elif data_type == "appointments": + data = get_appointments_as_array() + else: + return e500_error(request) writer = csv.writer(response, quotechar=str(u'"'), quoting=csv.QUOTE_ALL) - if type == "subjects": - write_subjects_to_csv(writer) - elif type == "appointments": - write_appointments_to_csv(writer) + for row in data: + writer.writerow([s.encode("utf-8") for s in row]) + + return response + + +@login_required +def export_to_excel(request, data_type="subjects"): + filename = data_type + '-' + get_today_midnight_date().strftime("%Y-%m-%d") + ".xls" + if data_type == "subjects": + data = get_subjects_as_array() + elif data_type == "appointments": + data = get_appointments_as_array() else: return e500_error(request) + + response = excel.make_response_from_array(data, 'xls', file_name=filename) + response['Content-Disposition'] = 'attachment; filename="' + filename + '"' + return response -def write_subjects_to_csv(writer): +def get_subjects_as_array(): + result = [] subject_fields = [] for field in Subject._meta.fields: if field.name != "ID": @@ -36,17 +57,22 @@ def write_subjects_to_csv(writer): for field in subject_fields: field_names.append(field.verbose_name) - writer.writerow(field_names) + result.append(field_names) subjects = Subject.objects.order_by('-last_name') for subject in subjects: row = [] for field in subject_fields: - row.append(getattr(subject, field.name)) - writer.writerow([unicode(s).replace("\n", ";").replace("\r", ";").encode("utf-8") for s in row]) + cell = getattr(subject, field.name) + if cell is None: + cell = "" + row.append(cell) + result.append([unicode(s).replace("\n", ";").replace("\r", ";") for s in row]) + return result -def write_appointments_to_csv(writer): +def get_appointments_as_array(): + result = [] appointments_fields = [] for field in Appointment._meta.fields: if field.name != "visit" and field.name != "id" and \ @@ -58,20 +84,24 @@ def write_appointments_to_csv(writer): for field in appointments_fields: field_names.append(field.verbose_name) - writer.writerow(field_names) + result.append(field_names) appointments = Appointment.objects.order_by('-datetime_when') for appointment in appointments: - row = [appointment.visit.subject.nd_number, appointment.visit.subject.last_name, - appointment.visit.subject.first_name, appointment.visit.follow_up_title()] + if appointment.visit is not None: + row = [appointment.visit.subject.nd_number, appointment.visit.subject.last_name, + appointment.visit.subject.first_name, appointment.visit.follow_up_title()] + else: + row = ["---", "---", "---", "---"] for field in appointments_fields: row.append(getattr(appointment, field.name)) type_string = "" - for type in appointment.appointment_types.all(): - type_string += type.code + "," + for appointment_type in appointment.appointment_types.all(): + type_string += appointment_type.code + "," row.append(type_string) - writer.writerow([unicode(s).replace("\n", ";").replace("\r", ";").encode("utf-8") for s in row]) + result.append([unicode(s).replace("\n", ";").replace("\r", ";") for s in row]) + return result def export(request): diff --git a/smash/web/views/notifications.py b/smash/web/views/notifications.py index 0d47a354c00a01796a5a656f2543a5cdf715ddf7..6a580bc56a8768fc0c11dc3443690296dae55915 100644 --- a/smash/web/views/notifications.py +++ b/smash/web/views/notifications.py @@ -1,5 +1,6 @@ # coding=utf-8 import datetime +from django.utils import timezone from django.contrib.auth.models import User, AnonymousUser from django.db.models import Count, Case, When @@ -239,6 +240,6 @@ def get_filter_locations(user): def get_today_midnight_date(): - today = datetime.datetime.now() - today_midnight = datetime.datetime(today.year, today.month, today.day) + today = timezone.now() + today_midnight = datetime.datetime(today.year, today.month, today.day, tzinfo=today.tzinfo) return today_midnight