diff --git a/ledger/accounts/test_models.py b/ledger/accounts/test_models.py index 33266452cf..6d307b0cbf 100644 --- a/ledger/accounts/test_models.py +++ b/ledger/accounts/test_models.py @@ -1,6 +1,6 @@ from django.test import TestCase from django_dynamic_fixture import get as get_ddf -from ledger.accounts.models import EmailUser, Address +from ledger.accounts.models import EmailUser, Address, Country class EmailUserTest(TestCase): @@ -50,7 +50,8 @@ class AddressTest(TestCase): def setUp(self): super(AddressTest, self).setUp() - self.address1 = get_ddf(Address) + get_ddf(Country, iso_3166_1_a2='AU') + self.address1 = get_ddf(Address, country='AU') def test_prop_join_fields(self): """Test the Address join_fields property diff --git a/requirements.txt b/requirements.txt index 2806db1528..efbaa84ed1 100755 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ django-cron==0.4.6 django-dynamic-fixture>=1.9.0 openpyxl==2.3.5 datapackage>=0.6.1 -jsontableschema>=0.6.3 +jsontableschema==0.6.5 python-dateutil==2.5.3 py4j==0.10.2.1 djangorestframework==3.4.0 diff --git a/wildlifelicensing/apps/applications/templates/wl/emails/licence_issued.html b/wildlifelicensing/apps/applications/templates/wl/emails/licence_issued.html index ae4463e4ec..d096069e30 100644 --- a/wildlifelicensing/apps/applications/templates/wl/emails/licence_issued.html +++ b/wildlifelicensing/apps/applications/templates/wl/emails/licence_issued.html @@ -18,7 +18,7 @@

- from Jim Sharp
+ for Jim Sharp
DIRECTOR GENERAL

diff --git a/wildlifelicensing/apps/applications/templates/wl/emails/licence_issued.txt b/wildlifelicensing/apps/applications/templates/wl/emails/licence_issued.txt index 2e698bdb34..6926236fcb 100644 --- a/wildlifelicensing/apps/applications/templates/wl/emails/licence_issued.txt +++ b/wildlifelicensing/apps/applications/templates/wl/emails/licence_issued.txt @@ -17,7 +17,7 @@ Yours sincerely -from Jim Sharp +for Jim Sharp DIRECTOR GENERAL diff --git a/wildlifelicensing/apps/applications/templates/wl/emails/renew_licence_notification.html b/wildlifelicensing/apps/applications/templates/wl/emails/renew_licence_notification.html index c38ee2356f..bff468917b 100644 --- a/wildlifelicensing/apps/applications/templates/wl/emails/renew_licence_notification.html +++ b/wildlifelicensing/apps/applications/templates/wl/emails/renew_licence_notification.html @@ -28,7 +28,7 @@

- from Jim Sharp
+ for Jim Sharp
DIRECTOR GENERAL

diff --git a/wildlifelicensing/apps/applications/templates/wl/emails/renew_licence_notification.txt b/wildlifelicensing/apps/applications/templates/wl/emails/renew_licence_notification.txt index b4a13a028f..7cf32e7069 100644 --- a/wildlifelicensing/apps/applications/templates/wl/emails/renew_licence_notification.txt +++ b/wildlifelicensing/apps/applications/templates/wl/emails/renew_licence_notification.txt @@ -21,7 +21,7 @@ Yours sincerely -from Jim Sharp +for Jim Sharp DIRECTOR GENERAL diff --git a/wildlifelicensing/apps/applications/templates/wl/issue/issue_licence.html b/wildlifelicensing/apps/applications/templates/wl/issue/issue_licence.html index bfd7bf6f0e..eaa0369249 100755 --- a/wildlifelicensing/apps/applications/templates/wl/issue/issue_licence.html +++ b/wildlifelicensing/apps/applications/templates/wl/issue/issue_licence.html @@ -158,9 +158,15 @@

Editable application fields (will appear on licence)

{% include 'wl/issue/extracted_field.html' with field=item %} + {% if item.help_text %} +

{{ item.help_text }}

+ {% endif %} {% else %} + {% if item.help_text %} +

{{ item.help_text }}

+ {% endif %} {% for item_group in item.children %}
{% with width=item_group|length|derive_col_width %} diff --git a/wildlifelicensing/apps/applications/tests/helpers.py b/wildlifelicensing/apps/applications/tests/helpers.py index 319a53adc7..14b363b044 100755 --- a/wildlifelicensing/apps/applications/tests/helpers.py +++ b/wildlifelicensing/apps/applications/tests/helpers.py @@ -1,20 +1,22 @@ from datetime import datetime from django.test import TestCase -from django_dynamic_fixture import get as get_ddf +from django_dynamic_fixture import G from django.core.urlresolvers import reverse, reverse_lazy -from ledger.accounts.models import Profile +from ledger.accounts.models import Profile, Address from wildlifelicensing.apps.applications.views.entry import LICENCE_TYPE_NUM_CHARS, LODGEMENT_NUMBER_NUM_CHARS from wildlifelicensing.apps.applications.models import Application, Assessment, Condition from wildlifelicensing.apps.main.tests.helpers import create_random_customer, get_or_create_licence_type, \ - SocialClient, get_or_create_default_assessor_group, get_or_create_default_officer + SocialClient, get_or_create_default_assessor_group, get_or_create_default_officer, create_default_country def create_profile(user): - return get_ddf(Profile, user=user) + create_default_country() + address = G(Address, user=user, country='AU') + return G(Profile, adress=address, user=user) def create_application(user=None, **kwargs): @@ -28,7 +30,7 @@ def create_application(user=None, **kwargs): kwargs['licence_type'] = get_or_create_licence_type() if 'data' not in kwargs: kwargs['data'] = {} - application = get_ddf(Application, **kwargs) + application = G(Application, **kwargs) return application diff --git a/wildlifelicensing/apps/applications/tests/test_entry.py b/wildlifelicensing/apps/applications/tests/test_entry.py index 2123841226..c3061a0b25 100755 --- a/wildlifelicensing/apps/applications/tests/test_entry.py +++ b/wildlifelicensing/apps/applications/tests/test_entry.py @@ -19,6 +19,7 @@ class ApplicationEntryTestCase(TestCase): fixtures = ['licences.json', 'catalogue.json', 'partner.json'] def setUp(self): + helpers.create_default_country() self.customer = get_or_create_default_customer() self.client = SocialClient() diff --git a/wildlifelicensing/apps/applications/utils.py b/wildlifelicensing/apps/applications/utils.py index 54ecaf316b..5271020016 100755 --- a/wildlifelicensing/apps/applications/utils.py +++ b/wildlifelicensing/apps/applications/utils.py @@ -84,12 +84,7 @@ def _extract_licence_fields_from_item(item, data, licence_fields): if 'children' not in item: # label / checkbox types are extracted differently so skip here if item['type'] not in ('label', 'checkbox'): - licence_field = { - 'name': item['name'], - 'label': item['licenceFieldLabel'] if 'licenceFieldLabel' in item else item['label'], - 'type': item['type'], - 'readonly': item.get('isLicenceFieldReadonly', False) - } + licence_field = _create_licence_field(item) if 'options' in item: licence_field['options'] = item['options'] @@ -99,13 +94,8 @@ def _extract_licence_fields_from_item(item, data, licence_fields): licence_fields.append(licence_field) else: - licence_field = { - 'name': item['name'], - 'label': item['licenceFieldLabel'] if 'licenceFieldLabel' in item else item['label'], - 'type': item['type'], - 'readonly': item.get('isLicenceFieldReadonly', False), - 'children': [] - } + licence_field = _create_licence_field(item) + licence_field['children'] = [] child_data = _extract_item_data(item['name'], data) @@ -139,13 +129,8 @@ def _extract_licence_fields_from_item(item, data, licence_fields): def _extract_label_and_checkboxes(current_item, items, data, licence_fields): - licence_field = { - 'name': current_item['name'], - 'label': current_item['licenceFieldLabel'] if 'licenceFieldLabel' in current_item else current_item['label'], - 'type': current_item['type'], - 'readonly': current_item.get('isLicenceFieldReadonly', False), - 'options': [] - } + licence_field = _create_licence_field(current_item) + licence_field['options'] = [] # find index of first checkbox after checkbox label within current item list checkbox_index = 0 @@ -170,6 +155,16 @@ def _extract_label_and_checkboxes(current_item, items, data, licence_fields): licence_fields.append(licence_field) +def _create_licence_field(item): + return { + 'name': item['name'], + 'type': item['type'], + 'label': item['licenceFieldLabel'] if 'licenceFieldLabel' in item else item['label'], + 'help_text': item.get('licenceFieldHelpText', ''), + 'readonly': item.get('isLicenceFieldReadonly', False) + } + + def _extract_item_data(name, data): def ___extract_item_data(name, data): if isinstance(data, dict): diff --git a/wildlifelicensing/apps/applications/views/entry.py b/wildlifelicensing/apps/applications/views/entry.py index ae9ff995d5..84a6628ca9 100755 --- a/wildlifelicensing/apps/applications/views/entry.py +++ b/wildlifelicensing/apps/applications/views/entry.py @@ -196,6 +196,8 @@ def get(self, request, *args, **kwargs): application.licence_type = WildlifeLicenceType.objects.get(id=self.args[0]) + application.data = None + application.variants.clear() for index, variant_id in enumerate(request.GET.getlist('variants', [])): diff --git a/wildlifelicensing/apps/main/fixtures/groups.json b/wildlifelicensing/apps/main/fixtures/groups.json index 90567b2aff..db4f4b29e4 100644 --- a/wildlifelicensing/apps/main/fixtures/groups.json +++ b/wildlifelicensing/apps/main/fixtures/groups.json @@ -10,5 +10,11 @@ "fields": { "name": "Assessors" } + }, + { + "model": "auth.Group", + "fields": { + "name": "API" + } } ] diff --git a/wildlifelicensing/apps/main/pdf.py b/wildlifelicensing/apps/main/pdf.py index 3f233c505d..c2c3683f53 100755 --- a/wildlifelicensing/apps/main/pdf.py +++ b/wildlifelicensing/apps/main/pdf.py @@ -37,6 +37,7 @@ DEFAULT_FONTNAME = 'Helvetica' BOLD_FONTNAME = 'Helvetica-Bold' +ITALIC_FONTNAME = 'Helvetica-Oblique' BOLD_ITALIC_FONTNAME = 'Helvetica-BoldOblique' VERY_LARGE_FONTSIZE = 14 @@ -79,6 +80,8 @@ rightIndent=PAGE_WIDTH / 10)) styles.add(ParagraphStyle(name='BoldLeft', fontName=BOLD_FONTNAME, fontSize=MEDIUM_FONTSIZE, alignment=enums.TA_LEFT)) styles.add(ParagraphStyle(name='BoldRight', fontName=BOLD_FONTNAME, fontSize=MEDIUM_FONTSIZE, alignment=enums.TA_RIGHT)) +styles.add(ParagraphStyle(name='ItalicLeft', fontName=ITALIC_FONTNAME, fontSize=MEDIUM_FONTSIZE, alignment=enums.TA_LEFT)) +styles.add(ParagraphStyle(name='ItalifRight', fontName=ITALIC_FONTNAME, fontSize=MEDIUM_FONTSIZE, alignment=enums.TA_RIGHT)) styles.add(ParagraphStyle(name='Center', alignment=enums.TA_CENTER)) styles.add(ParagraphStyle(name='Left', alignment=enums.TA_LEFT)) styles.add(ParagraphStyle(name='Right', alignment=enums.TA_RIGHT)) @@ -264,6 +267,11 @@ def _layout_extracted_fields(extracted_fields): elements.append(Spacer(1, SECTION_BUFFER_HEIGHT)) elements.append(Paragraph(field['label'], styles['BoldLeft'])) + if field['help_text']: + elements.append(Paragraph(field['help_text'], styles['ItalicLeft'])) + + elements.append(Spacer(1, SECTION_BUFFER_HEIGHT)) + if field['type'] in ['text', 'text_area']: elements += _layout_paragraphs(field['data']) elif field['type'] in ['radiobuttons', 'select']: @@ -276,6 +284,12 @@ def _layout_extracted_fields(extracted_fields): if any([option.get('data', 'off') == 'on' for option in field['options']]): elements.append(Spacer(1, SECTION_BUFFER_HEIGHT)) elements.append(Paragraph(field['label'], styles['BoldLeft'])) + + if field['help_text']: + elements.append(Paragraph(field['help_text'], styles['ItalicLeft'])) + + elements.append(Spacer(1, SECTION_BUFFER_HEIGHT)) + elements.append(Paragraph(', '.join([option['label'] for option in field['options'] if option.get('data', 'off') == 'on']), styles['Left'])) @@ -283,6 +297,9 @@ def _layout_extracted_fields(extracted_fields): elements.append(Spacer(1, SECTION_BUFFER_HEIGHT)) elements.append(Paragraph(field['label'], styles['BoldLeft'])) + if field['help_text']: + elements.append(Paragraph(field['help_text'], styles['ItalicLeft'])) + table_data = [] for index, group in enumerate(field['children']): if index == 0: @@ -398,7 +415,7 @@ def _create_letter_signature(): signature_elements = [] signature_elements.append(Paragraph('Yours sincerely', styles['LetterLeft'])) signature_elements.append(Spacer(1, SECTION_BUFFER_HEIGHT * 4)) - signature_elements.append(Paragraph('from Jim Sharp', styles['LetterLeft'])) + signature_elements.append(Paragraph('for Jim Sharp', styles['LetterLeft'])) signature_elements.append(Paragraph('DIRECTOR GENERAL', styles['LetterLeft'])) signature_elements.append(Spacer(1, SECTION_BUFFER_HEIGHT)) diff --git a/wildlifelicensing/apps/main/tests/helpers.py b/wildlifelicensing/apps/main/tests/helpers.py index b8640dafaa..6713c7f663 100644 --- a/wildlifelicensing/apps/main/tests/helpers.py +++ b/wildlifelicensing/apps/main/tests/helpers.py @@ -8,9 +8,9 @@ from django.core.urlresolvers import reverse from django.test import Client, TestCase from django.utils.encoding import smart_text -from django_dynamic_fixture import get as get_ddf +from django_dynamic_fixture import G -from ledger.accounts.models import EmailUser, Profile, Address +from ledger.accounts.models import EmailUser, Profile, Address, Country from wildlifelicensing.apps.main import helpers as accounts_helpers from wildlifelicensing.apps.main.models import WildlifeLicenceType, WildlifeLicence, AssessorGroup @@ -48,6 +48,12 @@ class TestData(object): 'name': 'ass group', 'email': 'assessor@test.com', } + DEFAULT_API_USER = { + 'email': 'apir@test.com', + 'first_name': 'api', + 'last_name': 'user', + 'dob': '1979-12-13', + } class SocialClient(Client): @@ -70,6 +76,10 @@ def logout(self): self.get(reverse('accounts:logout')) +def create_default_country(): + return G(Country, iso_3166_1_a2='AU') + + def is_client_authenticated(client): return '_auth_user_id' in client.session @@ -92,7 +102,7 @@ def get_or_create_user(email, defaults): def create_random_user(): - return get_ddf(EmailUser, dob='1970-01-01') + return G(EmailUser, dob='1970-01-01') def create_random_customer(): @@ -117,6 +127,13 @@ def get_or_create_default_officer(): return user +def get_or_create_api_user(): + user, created = get_or_create_user(TestData.DEFAULT_API_USER['email'], TestData.DEFAULT_API_USER) + if created: + add_to_group(user, 'API') + return user + + def get_or_create_licence_type(product_code='regulation-17'): return WildlifeLicenceType.objects.get_or_create(product_code=product_code)[0] diff --git a/wildlifelicensing/apps/main/tests/tests.py b/wildlifelicensing/apps/main/tests/tests.py index 0ed0423f5f..2dc20f773b 100644 --- a/wildlifelicensing/apps/main/tests/tests.py +++ b/wildlifelicensing/apps/main/tests/tests.py @@ -7,13 +7,14 @@ from ledger.accounts.models import EmailUser, Profile from wildlifelicensing.apps.main.tests.helpers import SocialClient, get_or_create_default_customer, \ - get_or_create_default_officer, TestData, upload_id + get_or_create_default_officer, TestData, upload_id, create_default_country TEST_ID_PATH = TestData.TEST_ID_PATH class AccountsTestCase(TestCase): def setUp(self): + create_default_country() self.customer = get_or_create_default_customer() self.officer = get_or_create_default_officer() diff --git a/wildlifelicensing/apps/main/views.py b/wildlifelicensing/apps/main/views.py index 3f4ec4ad40..70865a96a3 100755 --- a/wildlifelicensing/apps/main/views.py +++ b/wildlifelicensing/apps/main/views.py @@ -190,15 +190,13 @@ def post(self, request, *args, **kwargs): original_last_name = emailuser.last_name emailuser_form = EmailUserForm(request.POST, instance=emailuser) if emailuser_form.is_valid(): - emailuser = emailuser_form.save(commit=False) + emailuser = emailuser_form.save() is_name_changed = any([original_first_name != emailuser.first_name, original_last_name != emailuser.last_name]) # send signal if either first name or last name is changed if is_name_changed: messages.warning(request, "Please upload new identification after you changed your name.") return redirect(self.identification_url) - elif not emailuser.identification: - messages.warning(request, "Please upload your identification.") else: messages.success(request, "User account was saved.") diff --git a/wildlifelicensing/apps/payments/utils.py b/wildlifelicensing/apps/payments/utils.py index aaf092f42d..5683344dd8 100644 --- a/wildlifelicensing/apps/payments/utils.py +++ b/wildlifelicensing/apps/payments/utils.py @@ -35,7 +35,7 @@ def __append_variant_codes(product_code, variant_group, current_variant_codes): return for variant in variant_group.variants.all(): - variant_code = '{}_{}'.format(product_code, variant.product_code) + variant_code = '{} {}'.format(product_code, variant.product_code) __append_variant_codes(variant_code, variant_group.child, variant_codes) @@ -50,8 +50,9 @@ def generate_product_code(application): product_code = application.licence_type.product_code if application.variants.exists(): - product_code += '_' + '_'.join(application.variants.through.objects.filter(application=application). - order_by('order').values_list('variant__product_code', flat=True)) + product_code = '{} {}'.format(product_code, + ' '.join(application.variants.through.objects.filter(application=application). + order_by('order').values_list('variant__product_code', flat=True))) return product_code diff --git a/wildlifelicensing/apps/payments/views.py b/wildlifelicensing/apps/payments/views.py index 180322fb27..961cbfd9d1 100644 --- a/wildlifelicensing/apps/payments/views.py +++ b/wildlifelicensing/apps/payments/views.py @@ -98,7 +98,9 @@ def get(self, request): data = { 'system': PAYMENT_SYSTEM_ID, 'start': start, - 'end': end + 'end': end, + 'banked_start': start, + 'banked_end': end } if 'items' in request.GET: data['items'] = True diff --git a/wildlifelicensing/apps/reports/templates/wl/reports.html b/wildlifelicensing/apps/reports/templates/wl/reports.html index 3d2f6dbcdb..6aef1a406b 100755 --- a/wildlifelicensing/apps/reports/templates/wl/reports.html +++ b/wildlifelicensing/apps/reports/templates/wl/reports.html @@ -125,7 +125,6 @@

Payments

-
diff --git a/wildlifelicensing/apps/returns/api/mixins.py b/wildlifelicensing/apps/returns/api/mixins.py new file mode 100644 index 0000000000..63b0b1022a --- /dev/null +++ b/wildlifelicensing/apps/returns/api/mixins.py @@ -0,0 +1,18 @@ +from django.contrib.auth.mixins import UserPassesTestMixin + +from wildlifelicensing.apps.main.helpers import belongs_to + + +def is_api_user(user): + return belongs_to(user, 'API') or user.is_superuser + + +class APIUserRequiredMixin(UserPassesTestMixin): + """ + Mixin uses for API view. + """ + # we don't want to be redirected to login page. + raise_exception = True + + def test_func(self): + return is_api_user(self.request.user) diff --git a/wildlifelicensing/apps/returns/api/views.py b/wildlifelicensing/apps/returns/api/views.py index 0086d63d4a..392f503e34 100644 --- a/wildlifelicensing/apps/returns/api/views.py +++ b/wildlifelicensing/apps/returns/api/views.py @@ -5,12 +5,20 @@ from django.http import HttpResponse, Http404, JsonResponse from django.shortcuts import get_object_or_404 from django.views.generic import View +from django.utils import timezone +from wildlifelicensing.apps.returns.api.mixins import APIUserRequiredMixin from wildlifelicensing.apps.returns.models import ReturnType, ReturnRow from wildlifelicensing.apps.returns.utils_schema import Schema +API_SESSION_TIMEOUT = 100 * 24 * 3600 # 100 days -class ExplorerView(View): + +def set_api_session_timeout(request): + request.session.set_expiry(API_SESSION_TIMEOUT) + + +class ExplorerView(APIUserRequiredMixin, View): """ Return a JSON representation of the ReturnTypes. The main goal of this view is to provide for every resources (ReturnTable) a link to download the data @@ -19,7 +27,15 @@ class ExplorerView(View): def get(self, request): queryset = ReturnType.objects.all() - results = [] + # for API purpose, increase the session timeout + set_api_session_timeout(request) + sessionid = self.request.session.session_key + payload = OrderedDict() + payload['auth'] = { + "sessionId": sessionid, + "expires": timezone.localtime(self.request.session.get_expiry_date()) + } + data = [] for rt in queryset: return_obj = OrderedDict({'id': rt.id}) licence_type = { @@ -30,28 +46,39 @@ def get(self, request): # resources resources = [] for idx, resource in enumerate(rt.resources): - resource_obj = OrderedDict() - resource_obj['name'] = resource.get('name', '') - resource_obj['data'] = request.build_absolute_uri(reverse('wl_returns:api:data', kwargs={ + url = request.build_absolute_uri(reverse('wl_returns:api:data', kwargs={ 'return_type_pk': rt.pk, 'resource_number': idx })) + resource_obj = OrderedDict() + resource_obj['name'] = resource.get('name', '') + resource_obj['data'] = url + resource_obj['python'] = "requests.get('{0}', cookies={{'sessionid':'{1}'}}).content".format( + url, + sessionid + ) + resource_obj['shell'] = "curl {0} --cookie 'sessionid={1}'".format( + url, + sessionid + ) resource_obj['schema'] = resource.get('schema', {}) resources.append(resource_obj) return_obj['resources'] = resources - results.append(return_obj) - - return JsonResponse(results, safe=False) + data.append(return_obj) + payload['data'] = data + return JsonResponse(payload, json_dumps_params={'indent': 2}, safe=False) -class ReturnsDataView(View): +class ReturnsDataView(APIUserRequiredMixin, View): """ Export returns data in CSV format. """ def get(self, request, *args, **kwargs): return_type = get_object_or_404(ReturnType, pk=kwargs.get('return_type_pk')) + # for API purpose, increase the session timeout + set_api_session_timeout(request) resource_number = kwargs.get('resource_number') if not resource_number: resource_number = 0 diff --git a/wildlifelicensing/apps/returns/static/wl/js/return_table.js b/wildlifelicensing/apps/returns/static/wl/js/return_table.js index a020b31225..0c2380a1b7 100644 --- a/wildlifelicensing/apps/returns/static/wl/js/return_table.js +++ b/wildlifelicensing/apps/returns/static/wl/js/return_table.js @@ -1,8 +1,80 @@ -define(['jQuery', 'datatables.net', 'datatables.bootstrap', 'datatables.datetime'], function ($) { +define([ + 'jQuery', + 'datatables.net', + 'datatables.bootstrap', + 'datatables.datetime', + 'bootstrap-3-typeahead' +], function ($) { "use strict"; + function querySpecies(speciesType, search, callback) { + var url = '/taxonomy/species_name', + params = {}, + promise; + if (speciesType) { + params.type = speciesType; + } + if (search) { + params.search = search; + } + url += '?' + $.param(params); + promise = $.get(url); + if (typeof callback === 'function') { + promise.then(callback); + } + return promise; + } + + function setSpeciesValid($field, valid) { + var validClass = 'text-success'; + if (valid) { + $field.addClass(validClass); + } else { + $field.removeClass(validClass); + } + } + + function validateSpeciesField($field, speciesType) { + // Rules: if only one species is returned from the api we consider the name to be valid. + // Trick: the species can be recorded as: species_name (common name) in this case the search will fail + // (species_name or common name but not both). We get rid of anything in parenthesis. + var value = $field.val(); + if (value) { + value = value.replace(/\s?\(.*\)/, ''); + querySpecies(speciesType, value.trim(), function (data) { + var valid = data && data.length === 1; + setSpeciesValid($field, valid); + }); + } + } + + function initSpeciesFields($parent) { + var $species_fields = $parent.find('input[data-species]'); + if ($species_fields.length > 0) { + $species_fields.each(function () { + var $field = $(this), + speciesType = $field.attr('data-species'), + value; + $field.typeahead({ + minLength: 2, + items: 'all', + source: function (query, process) { + querySpecies(speciesType, query, function (data) { + return process(data); + }); + } + }); + value = $field.val(); + if (value) { + // already some data. We try to validate. + validateSpeciesField($field, speciesType); + } + }); + } + } + return { - initTables: function() { + initTables: function () { var $tables = $('.return-table'), $curationForm = $('#curationForm'); @@ -13,7 +85,9 @@ define(['jQuery', 'datatables.net', 'datatables.bootstrap', 'datatables.datetime info: false }); - $('.add-return-row').click(function() { + initSpeciesFields($tables); + + $('.add-return-row').click(function () { var $tbody = $(this).parent().find('table').find('tbody'), $row = $tbody.find('tr:first'); // clone the top row @@ -25,14 +99,15 @@ define(['jQuery', 'datatables.net', 'datatables.bootstrap', 'datatables.datetime // append cloned row $tbody.append($rowCopy); + initSpeciesFields($rowCopy); }); - $('#accept').click(function() { + $('#accept').click(function () { $curationForm.append($('').attr('name', 'accept').addClass('hidden')); $curationForm.submit(); }); - $('#decline').click(function() { + $('#decline').click(function () { $curationForm.append($('').attr('name', 'decline').addClass('hidden')); $curationForm.submit(); }); diff --git a/wildlifelicensing/apps/returns/templates/wl/curate_return.html b/wildlifelicensing/apps/returns/templates/wl/curate_return.html index 99f7c97cb3..2efa4537f5 100755 --- a/wildlifelicensing/apps/returns/templates/wl/curate_return.html +++ b/wildlifelicensing/apps/returns/templates/wl/curate_return.html @@ -115,7 +115,7 @@

{{ table.title }}

{% for header in table.headers %} - {{ header }} + {{ header.title }}{% if header.required %} *{% endif %} {% endfor %} @@ -123,10 +123,14 @@

{{ table.title }}

{% for row in table.data %} {% for header in table.headers %} - {% with value=row|get_item:header|get_item:'value' error=row|get_item:header|get_item:'error' %} + {% with value=row|get_item:header.title|get_item:'value' error=row|get_item:header.title|get_item:'error' %} - + {% if error %}
{{ error }}
diff --git a/wildlifelicensing/apps/returns/templates/wl/enter_return.html b/wildlifelicensing/apps/returns/templates/wl/enter_return.html index 423c2cafc4..8c0bac6df2 100755 --- a/wildlifelicensing/apps/returns/templates/wl/enter_return.html +++ b/wildlifelicensing/apps/returns/templates/wl/enter_return.html @@ -18,7 +18,11 @@ {% block requirements %} require(["{% static 'wl/js/return_table.js' %}"], function (returnTable) { - returnTable.initTables(); + returnTable.initTables(); + // disable form submit by 'enter' key + $(document).on("keypress", "form", function(event) { + return event.keyCode != 13; + }); }); {% endblock %} @@ -120,7 +124,11 @@

{{ table.title }}

{% with value=row|get_item:header.title|get_item:'value' error=row|get_item:header.title|get_item:'error' %} + value="{{ value|default_if_none:"" }}" + {% if header.species %} + data-species="{{ header.species }}" + {% endif %} + /> {% if error %}
{{ error }}
{% endif %} @@ -134,7 +142,11 @@

{{ table.title }}

{% for header in table.headers %} - + {% endfor %} diff --git a/wildlifelicensing/apps/returns/tests/helpers.py b/wildlifelicensing/apps/returns/tests/helpers.py index e48783f339..b3dd4caba5 100644 --- a/wildlifelicensing/apps/returns/tests/helpers.py +++ b/wildlifelicensing/apps/returns/tests/helpers.py @@ -1,8 +1,107 @@ +import copy from datetime import date, timedelta from wildlifelicensing.apps.returns.models import Return, ReturnType +def clone(descriptor): + return copy.deepcopy(descriptor) + + +BASE_CONSTRAINTS = { + "required": False +} + +NOT_REQUIRED_CONSTRAINTS = { + "required": False +} + +REQUIRED_CONSTRAINTS = { + "required": True +} + +BASE_FIELD = { + "name": "Name", + "tile": "Title", + "type": "string", + "format": "default", + "constraints": clone(BASE_CONSTRAINTS) +} + +GENERIC_SCHEMA = { + "fields": [ + clone(BASE_FIELD) + ] +} + +GENERIC_DATA_PACKAGE = { + "name": "test", + "resources": [ + { + "name": "test", + "format": "CSV", + "title": "test", + "bytes": 0, + "mediatype": "text/csv", + "path": "test.csv", + "schema": clone(GENERIC_SCHEMA) + } + ], + "title": "Test" +} + +SPECIES_NAME_FIELD = { + "name": "Species Name", + "type": "string", + "format": "default", + "constraints": { + "required": True + }, + "wl": { + "type": "species" + } +} + +LAT_LONG_OBSERVATION_SCHEMA = { + "fields": [ + { + "name": "Observation Date", + "type": "date", + "format": "any", + "constraints": { + "required": True, + } + }, + { + "name": "Latitude", + "type": "number", + "format": "default", + "constraints": { + "required": True, + "minimum": -90.0, + "maximum": 90.0, + } + }, + { + "name": "Longitude", + "type": "number", + "format": "default", + "constraints": { + "required": True, + "minimum": -180.0, + "maximum": 180.0, + } + }, + ] +} + +SPECIES_SCHEMA = clone(LAT_LONG_OBSERVATION_SCHEMA) +SPECIES_SCHEMA['fields'].append(clone(SPECIES_NAME_FIELD)) + +SPECIES_DATA_PACKAGE = clone(GENERIC_DATA_PACKAGE) +SPECIES_DATA_PACKAGE['resources'][0]['schema'] = clone(SPECIES_SCHEMA) + + def get_or_create_return_type(licence_type): return ReturnType.objects.get_or_create(licence_type=licence_type)[0] diff --git a/wildlifelicensing/apps/returns/tests/test_api.py b/wildlifelicensing/apps/returns/tests/test_api.py new file mode 100644 index 0000000000..b722794d46 --- /dev/null +++ b/wildlifelicensing/apps/returns/tests/test_api.py @@ -0,0 +1,103 @@ +from django.test import TestCase +from django.core.urlresolvers import reverse +from rest_framework import status + +from wildlifelicensing.apps.returns.models import ReturnType +from wildlifelicensing.apps.main.tests import helpers +from wildlifelicensing.apps.returns.api.mixins import is_api_user + + +class TestExplorerView(TestCase): + fixtures = [ + 'countries', + 'groups', + 'licences', + 'conditions', + 'default-conditions', + 'returns' + ] + + def test_authorisation(self): + """ + Only superuser or API users + :return: + """ + url = reverse("wl_returns:api:explorer") + customer = helpers.get_or_create_default_customer() + officer = helpers.get_or_create_default_officer() + assessor = helpers.get_or_create_default_assessor() + + api_user = helpers.get_or_create_api_user() + self.assertTrue(is_api_user(api_user)) + + admin = helpers.create_random_customer() + admin.is_superuser = True + admin.save() + self.assertTrue(is_api_user(admin)) + + client = helpers.SocialClient() + forbidden = [customer, officer, assessor] + for user in forbidden: + client.login(user.email) + self.assertEqual(client.get(url).status_code, + status.HTTP_403_FORBIDDEN) + client.logout() + + allowed = [admin, api_user] + for user in allowed: + client.login(user.email) + self.assertEqual(client.get(url).status_code, + status.HTTP_200_OK) + client.logout() + + +class TestDataView(TestCase): + fixtures = [ + 'countries', + 'groups', + 'licences', + 'conditions', + 'default-conditions', + 'returns' + ] + + def setUp(self): + # should have at least on return type + self.return_type = ReturnType.objects.first() + self.assertIsNotNone(self.return_type) + + def test_authorisation(self): + """ + Only superuser or API users + :return: + """ + url = reverse("wl_returns:api:data", kwargs={ + 'return_type_pk': self.return_type.pk, + 'resource_number': 0 + }) + customer = helpers.get_or_create_default_customer() + officer = helpers.get_or_create_default_officer() + assessor = helpers.get_or_create_default_assessor() + + api_user = helpers.get_or_create_api_user() + self.assertTrue(is_api_user(api_user)) + + admin = helpers.create_random_customer() + admin.is_superuser = True + admin.save() + self.assertTrue(is_api_user(admin)) + + client = helpers.SocialClient() + forbidden = [customer, officer, assessor] + for user in forbidden: + client.login(user.email) + self.assertEqual(client.get(url).status_code, + status.HTTP_403_FORBIDDEN) + client.logout() + + allowed = [admin, api_user] + for user in allowed: + client.login(user.email) + self.assertEqual(client.get(url).status_code, + status.HTTP_200_OK) + client.logout() diff --git a/wildlifelicensing/apps/returns/tests/test_schema.py b/wildlifelicensing/apps/returns/tests/test_schema.py new file mode 100644 index 0000000000..f04db7d38e --- /dev/null +++ b/wildlifelicensing/apps/returns/tests/test_schema.py @@ -0,0 +1,250 @@ +import datetime + +from django.utils import six +from django.test import TestCase + +from wildlifelicensing.apps.returns.tests import helpers +from wildlifelicensing.apps.returns.tests.helpers import BASE_CONSTRAINTS, clone, BASE_FIELD, REQUIRED_CONSTRAINTS +from wildlifelicensing.apps.returns.utils_schema import SchemaConstraints, FieldSchemaError, SchemaField, Schema + + +class TestSchemaConstraints(TestCase): + def test_none_or_empty(self): + """ + None or empty is accepted + """ + self.assertEquals({}, SchemaConstraints(None).data) + self.assertEquals({}, SchemaConstraints({}).data) + + def test_required_property(self): + # no constraints -> require = False + self.assertFalse(SchemaConstraints(None).required) + cts = clone(BASE_CONSTRAINTS) + self.assertFalse(cts['required']) + self.assertFalse(SchemaConstraints(cts).required) + + cts = clone(BASE_CONSTRAINTS) + cts['required'] = True + self.assertTrue(cts['required']) + self.assertTrue(SchemaConstraints(cts).required) + + def test_get_method(self): + """ + test that the SchemaField has the dict-like get('key', default) + """ + cts = clone(BASE_CONSTRAINTS) + sch = SchemaConstraints(cts) + self.assertTrue(hasattr(sch, 'get')) + self.assertEquals(cts.get('required'), sch.get('required')) + self.assertEquals(cts.get('constraints'), sch.get('constraints')) + self.assertEquals(None, sch.get('bad_keys')) + self.assertEquals('default', sch.get('bad_keys', 'default')) + + +class TestSchemaField(TestCase): + def setUp(self): + self.base_field = clone(BASE_FIELD) + + def test_name_mandatory(self): + """ + A schema field without name should throw an exception + """ + field = self.base_field + del field['name'] + with self.assertRaises(FieldSchemaError): + SchemaField(field) + # no blank + field = self.base_field + field['name'] = '' + with self.assertRaises(FieldSchemaError): + SchemaField(field) + + def test_get_method(self): + """ + test that the SchemaField has the dict-like get('key', default) + """ + field = self.base_field + sch = SchemaField(field) + self.assertTrue(hasattr(sch, 'get')) + self.assertEquals(field.get('Name'), sch.get('Name')) + self.assertEquals(field.get('constraints'), sch.get('constraints')) + self.assertEquals(None, sch.get('bad_keys')) + self.assertEquals('default', sch.get('bad_keys', 'default')) + + def test_column_name(self): + """ + 'column_name' is a property that is equal to name + """ + field = self.base_field + sch = SchemaField(field) + self.assertEquals(sch.name, sch.column_name) + self.assertNotEqual(sch.column_name, sch.title) + + def test_constraints(self): + """ + test that the constraints property returned a SchemaConstraints + """ + self.assertIsInstance(SchemaField(BASE_FIELD).constraints, SchemaConstraints) + + +class TestSchemaFieldCast(TestCase): + def setUp(self): + self.base_field_descriptor = clone(BASE_FIELD) + + def test_boolean(self): + true_values = [True, 'True', 'true', 'YES', 'yes', 'y', 't', '1', 1] + false_values = [False, 'FALSE', 'false', 'NO', 'no', 'n', 'f', '0', 0] + wrong_values = [2, 3, 'FLSE', 'flse', 'NON', 'oui', 'maybe', 'not sure'] + descriptor = self.base_field_descriptor + descriptor['type'] = 'boolean' + # only 'default' format + descriptor['format'] = 'default' + f = SchemaField(descriptor) + for v in true_values: + self.assertTrue(f.cast(v)) + for v in false_values: + self.assertFalse(f.cast(v)) + for v in wrong_values: + with self.assertRaises(Exception): + f.cast(v) + + def test_date(self): + descriptor = clone(BASE_FIELD) + descriptor['type'] = 'date' + # 'default' format = ISO + descriptor['format'] = 'default' + f = SchemaField(descriptor) + valid_values = ['2016-07-29'] + for v in valid_values: + date = f.cast(v) + self.assertIsInstance(date, datetime.date) + self.assertEqual(datetime.date(2016, 7, 29), date) + invalid_value = ['29/07/2016', '07/29/2016', '2016-07-29 15:28:37'] + for v in invalid_value: + with self.assertRaises(Exception): + f.cast(v) + + # format='any'. + # The main problem is to be sure that a dd/mm/yyyy is not interpreted as mm/dd/yyyy + descriptor['format'] = 'any' + f = SchemaField(descriptor) + valid_values = [ + '2016-07-10', + '10/07/2016', + '10/07/16', + '2016-07-10 15:28:37', + '10-July-2016', + '10-JUlY-16', + '10-07-2016', + '10-07-16' + ] + expected_date = datetime.date(2016, 7, 10) + for v in valid_values: + date = f.cast(v) + self.assertIsInstance(date, datetime.date) + self.assertEqual(expected_date, date) + invalid_value = ['djskdj'] + for v in invalid_value: + with self.assertRaises(Exception): + f.cast(v) + + def test_date_custom_format(self): + format_ = 'fmt:%d %b %y' # ex 30 Nov 14 + descriptor = { + 'name': 'Date with fmt', + 'type': 'date', + 'format': format_ + } + field = SchemaField(descriptor) + value = '30 Nov 14' + self.assertEqual(field.cast(value), datetime.date(2014, 11, 30)) + + def test_string(self): + # test that a blank string '' is not accepted when the field is required + null_values = ['null', 'none', 'nil', 'nan', '-', ''] + desc = clone(BASE_FIELD) + desc['type'] = 'string' + desc['constraints'] = clone(REQUIRED_CONSTRAINTS) + f = SchemaField(desc) + for v in null_values: + with self.assertRaises(Exception): + f.cast(v) + + # test non unicode (python 2) + value = 'not unicode' + self.assertIsInstance(f.cast(value), six.text_type) + self.assertEqual(f.cast(value), value) + + def test_datetime_any(self): + """ + test datetime field with 'any' format + :return: + """ + descriptor = clone(BASE_FIELD) + descriptor['type'] = 'datetime' + # format='any'. + # The main problem is to be sure that a dd/mm/yyyy is not interpreted as mm/dd/yyyy + descriptor['format'] = 'any' + f = SchemaField(descriptor) + valid_values = [ + '2016-07-10 13:55:00', + '10/07/2016 13:55', + '10/07/16 1:55 pm', + '2016-07-10 13:55:00', + '10-July-2016 13:55:00', + '10-JUlY-16 13:55:00', + '10-07-2016 13:55:00', + '10-07-16 13:55:00' + ] + expected_dt = datetime.datetime(2016, 7, 10, 13, 55, 00) + for v in valid_values: + dt = f.cast(v) + self.assertIsInstance(dt, datetime.datetime) + self.assertEqual(expected_dt, dt) + invalid_value = ['djskdj'] + for v in invalid_value: + with self.assertRaises(Exception): + f.cast(v) + + +class TestSpeciesField(TestCase): + + def test_no_species(self): + descriptor = clone(helpers.GENERIC_SCHEMA) + sch = Schema(descriptor) + # no species field + self.assertFalse(sch.species_fields) + + def test_species_by_field_name(self): + """ + A previous implementation supported species field detection by just the field name. + Not anymore + :return: + """ + names = ['species name', 'Species Name', 'SPECIES_NAME', 'species_Name'] + for name in names: + descriptor = clone(helpers.GENERIC_SCHEMA) + sch = Schema(descriptor) + # no species field + self.assertFalse(sch.species_fields) + # add a field named name + field = clone(BASE_FIELD) + field['name'] = name + descriptor['fields'].append(field) + sch = Schema(descriptor) + self.assertEqual(0, len(sch.species_fields)) + + def test_species_by_wl_tag(self): + # adding a wl species tag to a field turns it into a species field (whatever its name) + descriptor = clone(helpers.GENERIC_SCHEMA) + sch = Schema(descriptor) + # no species field + self.assertFalse(sch.species_fields) + # tag + field = descriptor['fields'][0] + field['wl'] = { + 'type': 'species' + } + sch = Schema(descriptor) + self.assertEqual(1, len(sch.species_fields)) + self.assertEquals(field['name'], sch.species_fields[0].name) diff --git a/wildlifelicensing/apps/returns/utils_schema.py b/wildlifelicensing/apps/returns/utils_schema.py index bfe685d0c3..fef2240195 100644 --- a/wildlifelicensing/apps/returns/utils_schema.py +++ b/wildlifelicensing/apps/returns/utils_schema.py @@ -1,5 +1,15 @@ +from __future__ import absolute_import, unicode_literals, print_function, division +from future.utils import raise_with_traceback + +import json +import re + +from dateutil.parser import parse as date_parse + from jsontableschema.model import SchemaModel from jsontableschema import types +from jsontableschema.exceptions import InvalidDateType + from openpyxl import Workbook from openpyxl.styles import Font from openpyxl.writer.write_only import WriteOnlyCell @@ -10,6 +20,106 @@ COLUMN_HEADER_FONT = Font(bold=True) +YYYY_MM_DD_REGEX = re.compile(r'^\d{4}-\d{2}-\d{2}') + + +class FieldSchemaError(Exception): + pass + + +def parse_datetime_day_first(value): + """ + use the dateutil.parse() to parse a date/datetime with the date first (dd/mm/yyyy) (not month first mm/dd/yyyy) + in case of ambiguity + :param value: + :return: + """ + # there's a 'bug' in dateutil.parser.parse (2.5.3). If you are using + # dayfirst=True. It will parse YYYY-MM-DD as YYYY-DD-MM !! + # https://github.com/dateutil/dateutil/issues/268 + dayfirst = not YYYY_MM_DD_REGEX.match(value) + return date_parse(value, dayfirst=dayfirst) + + +class DayFirstDateType(types.DateType): + """ + Extend the jsontableschema DateType which use the mm/dd/yyyy date model for the 'any' format + to use dd/mm/yyyy. + """ + + def cast_any(self, value, fmt=None): + if isinstance(value, self.python_type): + return value + try: + return parse_datetime_day_first(value).date() + except (TypeError, ValueError) as e: + raise_with_traceback(InvalidDateType(e)) + + +class DayFirstDateTimeType(types.DateTimeType): + """ + Extend the jsontableschema DateType which use the mm/dd/yyyy date model for the 'any' format + to use dd/mm/yyyy + """ + + def cast_any(self, value, fmt=None): + if isinstance(value, self.python_type): + return value + try: + return parse_datetime_day_first(value) + except (TypeError, ValueError) as e: + raise_with_traceback(InvalidDateType(e)) + + +class NotBlankStringType(types.StringType): + """ + The default StringType accepts empty string when required = True + """ + null_values = ['null', 'none', 'nil', 'nan', '-', ''] + + +@python_2_unicode_compatible +class WLSchema: + """ + The utility class for the wildlife licensing data within a schema field + Use to tag a filed to be a species field + { + name: "...." + constraints: .... + wl: { + type: "species" + speciesType: 'fauna'|'flora'|'all' + } + } + """ + SPECIES_TYPE_NAME = 'species' + SPECIES_TYPE_FLORA_NAME = 'flora' + SPECIES_TYPE_FAUNA_NAME = 'fauna' + + def __init__(self, data): + self.data = data or {} + + # implement some dict like methods + def __getitem__(self, item): + return self.data.__getitem__(item) + + def __str__(self): + return "WLSchema: {}".format(self.data) + + @property + def type(self): + return self.get('type') + + @property + def species_type(self): + return self.get('speciesType') + + def get(self, k, d=None): + return self.data.get(k, d) + + def is_species_type(self): + return self.type == self.SPECIES_TYPE_NAME + @python_2_unicode_compatible class SchemaField: @@ -19,24 +129,59 @@ class SchemaField: https://github.com/frictionlessdata/jsontableschema-py#types for validation. """ + # For most of the type we use the jsontableschema ones + BASE_TYPE_MAP = SchemaModel._type_map() + # except for anything date. + BASE_TYPE_MAP['date'] = DayFirstDateType + BASE_TYPE_MAP['datetime'] = DayFirstDateTimeType + # and string + BASE_TYPE_MAP['string'] = NotBlankStringType + + WL_TYPE_MAP = { + } def __init__(self, data): self.data = data - self.name = data['name'] # We want to throw an exception if there is no name - # use of jsontableschema.types to help constraint validation - self.type = SchemaModel._type_map()[data.get('type')](data) + self.name = self.data.get('name') + # We want to throw an exception if there is no name + if not self.name: + raise FieldSchemaError("A field without a name: {}".format(json.dumps(data))) + # wl specific + self.wl = WLSchema(self.data.get('wl')) + # set the type: wl type as precedence + type_class = self.WL_TYPE_MAP.get(self.wl.type) or self.BASE_TYPE_MAP.get(self.data.get('type')) + self.type = type_class(self.data) + self.constraints = SchemaConstraints(self.data.get('constraints', {})) + + # implement some dict like methods + def __getitem__(self, item): + return self.data.__getitem__(item) + + def get(self, k, d=None): + return self.data.get(k, d) + + @property + def title(self): + return self.data.get('title') @property def column_name(self): return self.name @property - def constraints(self): - return self.data.get('constraints', {}) + def required(self): + return self.constraints.required @property - def required(self): - return self.constraints.get('required', False) + def is_species(self): + return self.wl.is_species_type() + + @property + def species_type(self): + result = None + if self.is_species: + return self.wl.species_type or 'all' + return result def cast(self, value): """ @@ -47,9 +192,6 @@ def cast(self, value): :param value: :return: """ - if is_blank_value(value): - # must do that because an empty string is considered as valid even if required by the StringType - value = None if isinstance(value, six.string_types) and not isinstance(value, six.text_type): # the StringType accepts only unicode value = six.u(value) @@ -91,6 +233,26 @@ def __str__(self): return '{}'.format(self.name) +class SchemaConstraints: + """ + A helper class for a schema field constraints + """ + + def __init__(self, data): + self.data = data or {} + + # implement some dict like methods + def __getitem__(self, item): + return self.data.__getitem__(item) + + def get(self, k, d=None): + return self.data.get(k, d) + + @property + def required(self): + return self.get('required', False) + + class Schema: """ A utility class for schema. @@ -99,8 +261,29 @@ class Schema: """ def __init__(self, schema): + self.data = schema self.schema_model = SchemaModel(schema) self.fields = [SchemaField(f) for f in self.schema_model.fields] + self.species_fields = self.find_species_fields(self) + + # implement some dict like methods + def __getitem__(self, item): + return self.data.__getitem__(item) + + def get(self, k, d=None): + return self.data.get(k, d) + + @staticmethod + def find_species_fields(schema): + """ + Precedence Rules: + 1- Look for field of wl.type = 'species' + :param schema: a dict descriptor or a Schema instance + :return: an array of [SchemaField] or [] + """ + if not isinstance(schema, Schema): + schema = Schema(schema) + return [f for f in schema.fields if f.is_species] @property def headers(self): diff --git a/wildlifelicensing/apps/returns/views.py b/wildlifelicensing/apps/returns/views.py index 80ab0b4ec8..19e7ca37fe 100755 --- a/wildlifelicensing/apps/returns/views.py +++ b/wildlifelicensing/apps/returns/views.py @@ -126,10 +126,20 @@ def get_context_data(self, **kwargs): for resource in ret.return_type.resources: resource_name = resource.get('name') schema = Schema(resource.get('schema')) - headers = [{"title": f.name, "required": f.required} for f in schema.fields] - table = {'name': resource_name, 'title': resource.get('title', resource.get('name')), - 'headers': headers} - + headers = [] + for f in schema.fields: + header = { + "title": f.name, + "required": f.required + } + if f.is_species: + header["species"] = f.species_type + headers.append(header) + table = { + 'name': resource_name, + 'title': resource.get('title', resource.get('name')), + 'headers': headers + } try: return_table = ret.returntable_set.get(name=resource_name) rows = [return_row.data for return_row in return_table.returnrow_set.all()] @@ -213,45 +223,25 @@ def post(self, request, *args, **kwargs): return render(request, self.template_name, context) -class CurateReturnView(OfficerRequiredMixin, TemplateView): +class CurateReturnView(EnterReturnView): template_name = 'wl/curate_return.html' login_url = '/' def get_context_data(self, **kwargs): - ret = get_object_or_404(Return, pk=self.args[0]) - - kwargs['return'] = serialize(ret, posthook=format_return) - - kwargs['tables'] = [] - - for resource in ret.return_type.resources: - resource_name = resource.get('name') - schema = Schema(resource.get('schema')) - table = {'name': resource_name, 'title': resource.get('title', resource.get('name')), - 'headers': schema.headers} - - try: - return_table = ret.returntable_set.get(name=resource_name) - rows = [return_row.data for return_row in return_table.returnrow_set.all()] - validated_rows = list(schema.rows_validator(rows)) - table['data'] = validated_rows - except ReturnTable.DoesNotExist: - pass - - kwargs['tables'].append(table) - - kwargs['upload_spreadsheet_form'] = UploadSpreadsheetForm() + ctx = super(CurateReturnView, self).get_context_data(**kwargs) + ret = ctx['return'] + ctx['return'] = serialize(ret, posthook=format_return) if ret.proxy_customer is None: to = ret.licence.holder else: to = ret.proxy_customer - kwargs['log_entry_form'] = ReturnsLogEntryForm(to=to.get_full_name(), - fromm=self.request.user.get_full_name(), - ) - - return super(CurateReturnView, self).get_context_data(**kwargs) + ctx['log_entry_form'] = ReturnsLogEntryForm( + to=to.get_full_name(), + fromm=self.request.user.get_full_name(), + ) + return ctx def post(self, request, *args, **kwargs): context = self.get_context_data()