Commits

Luke Plant committed be01322 Merge

Merged from default

Comments (0)

Files changed (9)

cciw/officers/tests/__init__.py

+from cciw.officers.tests.applicationform import *
 from cciw.officers.tests.applications import *
+from cciw.officers.tests.officerlist import *
 from cciw.officers.tests.references import *
-from cciw.officers.tests.applicationform import *
 from cciw.officers.tests.utils import *

cciw/officers/tests/officerlist.py

+from django.contrib.auth.models import User
+from django.test import TestCase
+import xlrd
+
+from cciw.cciwmain.models import Camp
+from cciw.officers.models import Invitation, Application
+from cciw.officers.tests.references import OFFICER, LEADER
+from cciw.officers.utils import officer_data_to_xls
+
+
+class TestExport(TestCase):
+
+    fixtures = ['basic.json', 'officers_users.json', 'references.json']
+
+    def test_export_no_application(self):
+        """
+        Test that the export data view generates an Excel file with all the data
+        we expect if there is no application form.
+        """
+        c = Camp.objects.get(pk=1)
+        officers = list(c.officers.all())
+        first_names = [o.first_name for o in officers]
+
+        # In this test, delete completed applications, so we can test what
+        # happens with no application.
+        Application.objects.all().delete()
+
+        for i, inv in enumerate(c.invitation_set.all()):
+            inv.notes = "Some notes %s" % i
+            inv.save()
+
+        workbook = officer_data_to_xls(c)
+
+        self.assertTrue(workbook is not None)
+        wkbk = xlrd.open_workbook(file_contents=workbook)
+        wksh = wkbk.sheet_by_index(0)
+
+        # Spot checks on different types of data
+        # From User model
+        self.assertEqual(wksh.cell(0, 0).value, "First name")
+        self.assertTrue(wksh.cell(1, 0).value in first_names)
+
+        # From Invitation model
+        self.assertEqual(wksh.cell(0, 3).value, "Notes")
+        self.assertTrue(wksh.cell(1, 3).value.startswith('Some notes'))
+
+    def test_export_with_application(self):
+        """
+        Test that the export data view generates an Excel file with all the data
+        we expect if there are application forms.
+        """
+        c = Camp.objects.get(pk=1)
+        officers = list(c.officers.all())
+
+        # Data from fixtures
+        u = User.objects.get(pk=2)
+        app = Application.objects.get(pk=1)
+        assert app.officer == u
+
+        workbook = officer_data_to_xls(c)
+
+        wkbk = xlrd.open_workbook(file_contents=workbook)
+        wksh = wkbk.sheet_by_index(0)
+
+        # Check data from Application model
+        self.assertEqual(wksh.cell(0, 4).value, "Address")
+        self.assertTrue(app.address_firstline in wksh.col_values(4))

cciw/officers/tests/references.py

     def test_page_officers_denied(self):
         self._twill_login(OFFICER)
         tc.go(make_django_url("cciw.officers.views.manage_references", year=2000, number=1))
-        # Currently we get redirected to /officers/ page if insufficient
-        # privileges.
-        self.assertEqual(tc.get_browser().get_url().split('?')[0], make_django_url("cciw.officers.views.index"))
+        tc.code(403)
         tc.notfind('For camp 2000-1')
 
 

cciw/officers/urls.py

     (r'^leaders/applications/(?P<year>\d{4})/(?P<number>\d+)/$', 'manage_applications'),
     (r'^leaders/references/(?P<year>\d{4})/(?P<number>\d+)/$', 'manage_references'),
     (r'^leaders/officer-list/(?P<year>\d{4})/(?P<number>\d+)/$', 'officer_list'),
+    (r'^leaders/export-officer-data/(?P<year>\d{4})/(?P<number>\d+)/$', 'export_officer_data'),
     (r'^leaders/remove-officer/(?P<year>\d{4})/(?P<number>\d+)/$', 'remove_officer'),
     (r'^leaders/add-officers/(?P<year>\d{4})/(?P<number>\d+)/$', 'add_officers'),
     (r'^leaders/update-officer/$', 'update_officer'),

cciw/officers/utils.py

 """
 Utility functions for officers app.
 """
+from datetime import date, datetime
+from StringIO import StringIO
+
+import xlwt
+
+from cciw.officers.applications import applications_for_camp
 
 
 def camp_officer_list(camp):
     finished_apps_ids = applications_for_camp(camp).values_list('officer__id', flat=True)
     return list(camp.officers.order_by('first_name', 'last_name', 'email').exclude(id__in=finished_apps_ids))
 
+
+def officer_data_to_xls(camp):
+    # All the data we need:
+    invites = camp.invitation_set.all().select_related('officer').order_by('officer__first_name',
+                                                                           'officer__last_name')
+    apps = applications_for_camp(camp)
+    app_dict = dict((app.officer.id, app) for app in apps)
+
+    # Attributes we need
+    app_attr_getter = lambda attr: lambda user, inv, app: getattr(app, attr) if app is not None else ''
+    columns = [('First name', lambda u, inv, app: u.first_name),
+               ('Last name', lambda u, inv, app: u.last_name),
+               ('E-mail', lambda u, inv, app: u.email),
+               ('Notes', lambda u, inv, app: inv.notes),
+               ('Address', app_attr_getter('address_firstline')),
+               ('Town', app_attr_getter('address_town')),
+               ('County', app_attr_getter('address_county')),
+               ('Post code', app_attr_getter('address_postcode')),
+               ('Country', app_attr_getter('address_country')),
+               ('Tel', app_attr_getter('address_tel')),
+               ('Mobile', app_attr_getter('address_mobile')),
+               ('Email', app_attr_getter('address_email')),
+               ('Birth date', app_attr_getter('birth_date')),
+               ]
+
+    wkbk = xlwt.Workbook(encoding='utf8')
+    wksh = wkbk.add_sheet("Officers")
+
+    # Headers:
+    font_header = xlwt.Font()
+    font_header.bold = True
+    style_header = xlwt.XFStyle()
+    style_header.font = font_header
+    for c, (header, f) in enumerate(columns):
+        wksh.write(0, c, header, style=style_header)
+
+    # Data:
+    date_style = xlwt.XFStyle()
+    date_style.num_format_str = 'YYYY/MM/DD'
+    for r, inv in enumerate(invites):
+        user = inv.officer
+        app = app_dict.get(user.id, None)
+        for c, (header, f) in enumerate(columns):
+            val = f(user, inv, app)
+            if isinstance(val, (datetime, date)):
+                style = date_style
+            else:
+                style = xlwt.Style.default_style
+            wksh.write(r + 1, c, val, style=style)
+
+    # Write out to string:
+    s = StringIO()
+    wkbk.save(s)
+    s.seek(0)
+    return s.read()

cciw/officers/views.py

 import datetime
 import operator
 import urlparse
+from functools import wraps
 
 from django import forms
 from django.conf import settings
 from django.contrib.admin.views.decorators import staff_member_required
 from django.contrib.auth import REDIRECT_FIELD_NAME
-from django.contrib.auth.decorators import user_passes_test
 from django.contrib.auth.models import User
 from django.contrib import messages
 from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
 from django.core.urlresolvers import reverse
 from django.core.validators import email_re
-from django.http import Http404, HttpResponseRedirect, HttpResponse
+from django.http import Http404, HttpResponseRedirect, HttpResponse, HttpResponseForbidden
 from django.shortcuts import render, get_object_or_404
 from django.template import RequestContext
 from django.template.loader import render_to_string
 from cciw.officers.email import make_update_email_hash, send_reference_request_email, make_ref_form_url, make_ref_form_url_hash, send_leaders_reference_email, send_nag_by_officer, send_crb_consent_problem_email
 from cciw.officers.widgets import ExplicitBooleanFieldSelect
 from cciw.officers.models import Application, Reference, ReferenceForm, Invitation, CRBApplication, CRBFormLog
-from cciw.officers.utils import camp_slacker_list
+from cciw.officers.utils import camp_slacker_list, officer_data_to_xls
 from cciw.officers.references import reference_form_info
 from cciw.utils.views import close_window_response
 from securedownload.views import access_folder_securely
             user.groups.filter(name=SECRETARY_GROUP_NAME)).exists() \
         or user.camps_as_admin.exists() > 0
 
-camp_admin_required = user_passes_test(_is_camp_admin)
+
+def user_passes_test_improved(test_func):
+    """
+    Like user_passes_test, but doesn't redirect user to login screen if they are
+    already logged in.
+    """
+    def decorator(view_func):
+        @wraps(view_func)
+        def _wrapped_view(request, *args, **kwargs):
+            if test_func(request.user):
+                return view_func(request, *args, **kwargs)
+            if request.user.is_authenticated():
+                return HttpResponseForbidden("<h1>Access denied</h1>")
+
+            path = request.build_absolute_uri()
+            # If the login url is the same scheme and net location then just
+            # use the path as the "next" url.
+            login_scheme, login_netloc = urlparse.urlparse(login_url or
+                                                           settings.LOGIN_URL)[:2]
+            current_scheme, current_netloc = urlparse.urlparse(path)[:2]
+            if ((not login_scheme or login_scheme == current_scheme) and
+                (not login_netloc or login_netloc == current_netloc)):
+                path = request.get_full_path()
+            from django.contrib.auth.views import redirect_to_login
+            return redirect_to_login(path, login_url, redirect_field_name)
+        return _wrapped_view
+    return decorator
+
+
+camp_admin_required = user_passes_test_improved(_is_camp_admin)
 
 
 def _is_cciw_secretary(user):
     return {'status':'success'}
 
 
+@staff_member_required
+@camp_admin_required
+def export_officer_data(request, year=None, number=None):
+    camp = _get_camp_or_404(year, number)
+    response = HttpResponse(officer_data_to_xls(camp), mimetype="application/vnd.ms-excel")
+    response['Content-Disposition'] = ('attachment; filename=camp-%d-%d-officers.xls'
+                                       % (camp.year, camp.number))
+    return response
+
+
 officer_files = access_folder_securely("officers",
                                        lambda request: _is_camp_officer(request.user))
 
 twill==0.9
 zc.lockfile==1.0.0
 south==0.7.2
+xlwt==0.7.2
+xlrd==0.7.1
+

templates/cciw/officers/leaders_index.html

 {% extends "cciw/officers/base.html" %}
+{% load url from future %}
 {% block title %}Leaders' tools | CCIW Officers {% endblock %}
 {% block content %}<div id="content-main">
 <h1>Leaders' tools</h1>
   references:</p>
 <ul>
   {% for year in statsyears %}
-    <li><a href="{% url cciw.officers.views.stats year=year %}">Stats {{ year }}</a></li>
+    <li><a href="{% url 'cciw.officers.views.stats' year=year %}">Stats {{ year }}</a></li>
   {% endfor %}
 </ul>
 

templates/cciw/officers/officer_list.html

 {% include "cciw/officers/officer_list_noapplicationform.html" %}
 </div>
 
+
+<h2>Export officer data</h2>
+
+<p>The officer list above, along with address details from this year's
+  application forms, can be downloaded from the link below:</p>
+
+<p><a href="{% url 'cciw.officers.views.export_officer_data' year=camp.year number=camp.number %}">Download officer data, camp {{ camp.number }}, {{ camp.year }}</a></p>
+
 </div> {# id=content-main #}
 
 <div id="id_add_officer_popup" class="inlinepopup">