From 76538bb426dffae6291574a86b062ee8ff5c6a47 Mon Sep 17 00:00:00 2001 From: Odumosu Dipo Date: Fri, 4 Jun 2021 14:17:28 +0100 Subject: [PATCH 1/6] fix: don't depend on MySQL's case-insensitivity --- locations/api.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/locations/api.py b/locations/api.py index d95a3f6..a336625 100644 --- a/locations/api.py +++ b/locations/api.py @@ -1,5 +1,7 @@ +from django.db.models import Q from rest_framework import generics -from locations.models import Location + +from locations.models import Location, LocationType from locations.serializers import LocationSerializer @@ -41,8 +43,13 @@ def get_queryset(self): if not type_names: return queryset.none() - types = type_names.split(u',') - queryset = queryset.filter(type__name__in=types) + types = [t.lower() for t in type_names.split(u',')] + terms = Q() + for typ in types: + terms | Q(name__iexact=typ) + + loc_types = LocationType.objects.filter(terms) + queryset = queryset.filter(type__in=loc_types) # filter on parent id parent_id = self.request.query_params.get(u'parent') From 9f6994a9ae608d3b6623668a86b9f9f967f1d982 Mon Sep 17 00:00:00 2001 From: Odumosu Dipo Date: Fri, 4 Jun 2021 14:17:57 +0100 Subject: [PATCH 2/6] feat: add utility method --- locations/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/locations/models.py b/locations/models.py index cf91ee8..6dc0ee2 100644 --- a/locations/models.py +++ b/locations/models.py @@ -195,6 +195,17 @@ def get_stock(self): stock = None return stock + + def latest_birth_report_time(self): + if self.type.name != 'RC': + return None + + try: + report = self.birthregistration_records.latest('time') + + return report.time + except Exception: + return None def get_locations_graph(reverse=False): From 446c284ef746b6cc19e4dd964e29eec3672426b7 Mon Sep 17 00:00:00 2001 From: Odumosu Dipo Date: Fri, 4 Jun 2021 14:20:23 +0100 Subject: [PATCH 3/6] feat: add django-widget-tweaks to packages --- requirements/base.txt | 1 + unicefng/settings.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements/base.txt b/requirements/base.txt index 95dc304..71f092c 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -14,6 +14,7 @@ django-redis==4.7.0 django-selectable==1.2.1 django-sendfile==0.3.11 django-subdomains==2.1.0 +django-widget-tweaks==1.4.8 djangorestframework==3.9.4 drf-yasg==1.17.1 fuzzywuzzy==0.12.0 diff --git a/unicefng/settings.py b/unicefng/settings.py index 0a73781..49ef6f8 100644 --- a/unicefng/settings.py +++ b/unicefng/settings.py @@ -219,6 +219,7 @@ "pipeline", "bootstrap_pagination", "drf_yasg", + "widget_tweaks", # RapidSMS "rapidsms", "rapidsms.backends.database", From d7e75fc906966b22be2aa345e5347c32b028a4c7 Mon Sep 17 00:00:00 2001 From: Odumosu Dipo Date: Fri, 4 Jun 2021 14:24:17 +0100 Subject: [PATCH 4/6] feat: add view for nonreporting centres also, fix issues with centre edit view+template --- locations/forms.py | 27 +++- .../templates/locations/center_edit.html | 5 +- .../locations/non_reporting_centre_list.html | 132 ++++++++++++++++++ locations/urls.py | 9 +- locations/views.py | 106 +++++++++++++- unicefng/templates/common/usermenu.html | 1 + 6 files changed, 265 insertions(+), 15 deletions(-) create mode 100644 locations/templates/locations/non_reporting_centre_list.html diff --git a/locations/forms.py b/locations/forms.py index da0fdee..c15bc67 100644 --- a/locations/forms.py +++ b/locations/forms.py @@ -1,9 +1,13 @@ +import calendar + from django import forms -from .models import Location + +from br.utils import get_report_year_range +from locations.models import Location def generate_edit_form(location, data=None): - state_choices = Location.objects.filter(type__name='state').values_list( + state_choices = Location.objects.filter(type__name='State').values_list( 'id', 'name') def clean_location_field(form, field_name, location_type): @@ -64,3 +68,22 @@ class CenterCreationForm(forms.Form): name = forms.CharField() lga = forms.ModelChoiceField(queryset=Location.objects.filter( type__name=u'LGA')) + + +def _get_year_choices(): + choices = [('', '----- Select year -----')] + year_range = get_report_year_range() + choices.extend([(yr, yr) for yr in range(year_range[0], year_range[1] + 1)]) + return choices + + +def _get_month_choices(): + choices = [('', '----- Select month -----')] + choices.extend([(i, calendar.month_abbr[i]) for i in range(1, 13)]) + return choices + + +class NonReportingCentresFilterForm(forms.Form): + location = forms.ModelChoiceField(queryset=Location.objects.filter(type__name__in=['State', 'LGA']), required=False) + year = forms.ChoiceField(choices=_get_year_choices, required=False) + month = forms.ChoiceField(choices=_get_month_choices, required=False) diff --git a/locations/templates/locations/center_edit.html b/locations/templates/locations/center_edit.html index c6eb2f1..4f34d79 100644 --- a/locations/templates/locations/center_edit.html +++ b/locations/templates/locations/center_edit.html @@ -1,5 +1,5 @@ {% extends 'base/layout.html' %} -{% load pipeline staticfiles %} +{% load pipeline staticfiles widget_tweaks %} {% block stylesheets %} {% stylesheet 'centers' %} {% endblock %} @@ -21,7 +21,7 @@
- {{ form.state }} + {% render_field form.state class='form-control' %}
@@ -46,6 +46,7 @@
{% endblock %} {% block scripts %} + {% javascript 'centers' %} + +{% endblock %} \ No newline at end of file diff --git a/locations/urls.py b/locations/urls.py index 121b708..17bc610 100755 --- a/locations/urls.py +++ b/locations/urls.py @@ -1,10 +1,11 @@ #!/usr/bin/env python # vim: ai ts=4 sts=4 et sw=4 from django.conf.urls import url -from locations.views import * +from locations import views urlpatterns = [ - url(r'^center/new/?$', CenterCreationView.as_view(), name='center_add'), - url(r'^centers/?$', CenterListView.as_view(), name='center_list'), - url(r'^center/(?P\d+)/?$', CenterUpdateView.as_view(), name='center_edit'), + url(r'^center/new/?$', views.CenterCreationView.as_view(), name='center_add'), + url(r'^centers/?$', views.CenterListView.as_view(), name='center_list'), + url(r'^non-reporting-centers/?$', views.NonReportingCentresView.as_view(), name='non_reporting_center_list'), + url(r'^center/(?P\d+)/?$', views.CenterUpdateView.as_view(), name='center_edit'), ] diff --git a/locations/views.py b/locations/views.py index 8127c6f..736e4bc 100644 --- a/locations/views.py +++ b/locations/views.py @@ -1,4 +1,5 @@ # vim: ai ts=4 sts=4 et sw=4 +import csv import json from django.contrib import messages @@ -7,17 +8,23 @@ from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import reverse, reverse_lazy from django.db import connection +from django.db.models import ( + Case, Count, IntegerField, OuterRef, Subquery, When) from django.forms.formsets import formset_factory -from django.http import HttpResponse, HttpResponseRedirect +from django.http import ( + HttpResponse, HttpResponseRedirect, StreamingHttpResponse) from django.shortcuts import get_object_or_404 +from django.utils.timezone import now from django.views.generic import ListView, FormView, TemplateView from drf_yasg.utils import swagger_auto_schema import pandas as pd from django.conf import settings +from br.models import BirthRegistration from locations import raw_queries -from locations.forms import generate_edit_form, CenterCreationForm +from locations.forms import ( + generate_edit_form, CenterCreationForm, NonReportingCentresFilterForm) from locations.filters import CenterFilterSet from locations.models import Facility, Location, LocationType @@ -57,11 +64,14 @@ def get_queryset(self): class CenterUpdateView( LoginRequiredMixin, PermissionRequiredMixin, FormView): + permission_required = 'locations.edit_location' template_name = 'locations/center_edit.html' def get_context_data(self, **kwargs): context = super(CenterUpdateView, self).get_context_data(**kwargs) + self.object = self.get_object() + context['page_title'] = 'Edit center: {}'.format(self.object.name) context[u'location'] = self.object @@ -77,8 +87,7 @@ def form_valid(self, form): center = Location.objects.get(pk=form.cleaned_data['id']) center.name = form.cleaned_data['name'] center.code = form.cleaned_data['code'] - center.parent = Location.objects.get_object_or_404( - pk=form.cleaned_data['lga']) + center.parent = get_object_or_404(Location, pk=form.cleaned_data['lga']) center.active = form.cleaned_data['active'] center.save() center.facilities.update( @@ -87,14 +96,14 @@ def form_valid(self, form): return HttpResponseRedirect(self.get_success_url()) - def get_form(self, form_class): - return generate_edit_form(self.object) + def get_form(self, form_class=None): + return generate_edit_form(self.get_object()) def get_queryset(self): return Location.objects.filter(type__name='RC') def get_success_url(self): - return reverse('center_list') + return reverse('locations:center_list') def post(self, request, *args, **kwargs): form = generate_edit_form(self.get_object(), request.POST) @@ -195,3 +204,86 @@ def facilities(request): facility_dataframe.to_csv(response, encoding='UTF-8', index=False) return response + + + +class CSVBuffer(object): + def write(self, value): + return value + + +def _get_rows(centres): + yield ['Centre', 'Code', 'LGA', 'State', 'Active?', 'Last report date'] + + for centre in centres: + last_report_time = centre.latest_birth_report_time() + yield [ + centre.name, + centre.code, + centre.parent.name, + centre.parent.parent.name, + 'Yes' if centre.active else 'No', + last_report_time.strftime('%d-%m-%Y') if last_report_time else 'N/A' + ] + + +class NonReportingCentresView(LoginRequiredMixin, ListView): + context_object_name = 'centres' + page_title = 'Non-reporting centres' + paginate_by = settings.PAGE_SIZE + template_name = 'locations/non_reporting_centre_list.html' + + def get(self, request, *args, **kwargs): + centres = self.get_queryset() + + year = request.GET.get('year') or None + month = request.GET.get('month') or None + + if request.GET.get('export'): + writer = csv.writer(CSVBuffer()) + response = StreamingHttpResponse((writer.writerow(row) for row in _get_rows(centres)), content_type='text/csv') + filename = 'non-reporting-centres-{}-{}.csv'.format( + year, + str(month).zfill(2) + ) + response['Content-Disposition'] = 'attachment; filename={}'.format( + filename) + + return response + + return super(NonReportingCentresView, self).get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super(NonReportingCentresView, self).get_context_data(**kwargs) + + context['page_title'] = self.page_title + context['filter_form'] = self.filter_form + context['year'] = self.request.GET.get('year') or now().year + context['month'] = self.request.GET.get('month') or '' + + return context + + def get_queryset(self): + self.filter_form = NonReportingCentresFilterForm(self.request.GET) + if self.filter_form.is_valid(): + filter_data = self.filter_form.cleaned_data + else: + filter_data = {} + + filter_kwargs = {} + year = filter_data.get('year') or now().year + filter_kwargs['birthregistration_records__time__year'] = int(year) + month = filter_data.get('month') + if month: + filter_kwargs['birthregistration_records__time__month'] = int(month) + + if len(filter_kwargs) == 0: + centres = Location.objects.none() + else: + centres = Location.objects.filter( + type__name='RC' + ).annotate( + cnt=Count(Case(When(then=1, **filter_kwargs))) + ).filter(cnt=0) + + return centres diff --git a/unicefng/templates/common/usermenu.html b/unicefng/templates/common/usermenu.html index d76c22c..95c7614 100644 --- a/unicefng/templates/common/usermenu.html +++ b/unicefng/templates/common/usermenu.html @@ -2,6 +2,7 @@ {% if user.is_authenticated %} {% if perms.br.change_birthregistration %}
  • BR reports
  • {% endif %} {% if perms.br.change_birthregistration %}
  • BR centers
  • {% endif %} +{% if perms.br.change_birthregistration %}
  • Non-reporting BR centers
  • {% endif %}
  • BR help
  • {% if perms.dr.change_deathreport %}
  • DR reports
  • {% endif %} From 93d6106894b836299bf443bf65ac659b0dc2d195 Mon Sep 17 00:00:00 2001 From: Odumosu Dipo Date: Fri, 4 Jun 2021 15:12:05 +0100 Subject: [PATCH 5/6] fix: fix typo --- locations/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locations/api.py b/locations/api.py index a336625..718c9de 100644 --- a/locations/api.py +++ b/locations/api.py @@ -46,7 +46,7 @@ def get_queryset(self): types = [t.lower() for t in type_names.split(u',')] terms = Q() for typ in types: - terms | Q(name__iexact=typ) + terms |= Q(name__iexact=typ) loc_types = LocationType.objects.filter(terms) queryset = queryset.filter(type__in=loc_types) From eea988c1111fe0f060c25baeed42c9f093cfb71b Mon Sep 17 00:00:00 2001 From: Odumosu Dipo Date: Fri, 4 Jun 2021 15:12:24 +0100 Subject: [PATCH 6/6] feat: spruce up list --- .../templates/locations/center_edit.html | 6 +-- .../locations/non_reporting_centre_list.html | 10 ++-- locations/views.py | 48 +++++++++++++++---- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/locations/templates/locations/center_edit.html b/locations/templates/locations/center_edit.html index 4f34d79..4ab84f0 100644 --- a/locations/templates/locations/center_edit.html +++ b/locations/templates/locations/center_edit.html @@ -13,11 +13,11 @@ {{ form.id }}
    - + {% render_field form.name class='form-control' %}
    - + {% render_field form.code class='form-control' %}
    @@ -46,7 +46,7 @@
    {% endblock %} {% block scripts %} - +{{ block.super }} {% javascript 'centers' %}