diff --git a/smash/web/algorithm/__init__.py b/smash/web/algorithm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..defa4efb79e533df509ea627c72f61a3e4e001b1 --- /dev/null +++ b/smash/web/algorithm/__init__.py @@ -0,0 +1,4 @@ +from luhn_algorithm import LuhnAlgorithm +from verhoeff_alogirthm import VerhoeffAlgorithm + +__all__ = [VerhoeffAlgorithm, LuhnAlgorithm] diff --git a/smash/web/algorithm/luhn_algorithm.py b/smash/web/algorithm/luhn_algorithm.py new file mode 100644 index 0000000000000000000000000000000000000000..c752bc333716c2b7e4081bae0875cdd2911ee02d --- /dev/null +++ b/smash/web/algorithm/luhn_algorithm.py @@ -0,0 +1,21 @@ +class LuhnAlgorithm(object): + def __init__(self): + pass + + @staticmethod + def luhn_checksum(card_number): + def digits_of(n): + return [int(d) for d in str(n)] + + digits = digits_of(card_number) + odd_digits = digits[-1::-2] + even_digits = digits[-2::-2] + checksum = 0 + checksum += sum(odd_digits) + for d in even_digits: + checksum += sum(digits_of(d * 2)) + return checksum % 10 + + @staticmethod + def is_luhn_valid(card_number): + return LuhnAlgorithm.luhn_checksum(card_number) == 0 diff --git a/smash/web/algorithm/verhoeff_alogirthm.py b/smash/web/algorithm/verhoeff_alogirthm.py new file mode 100644 index 0000000000000000000000000000000000000000..0b9dc9ffcf0b5026bfa95845efc71b34fd8b03d4 --- /dev/null +++ b/smash/web/algorithm/verhoeff_alogirthm.py @@ -0,0 +1,45 @@ +verhoeff_multiplication_table = ( + (0, 1, 2, 3, 4, 5, 6, 7, 8, 9), + (1, 2, 3, 4, 0, 6, 7, 8, 9, 5), + (2, 3, 4, 0, 1, 7, 8, 9, 5, 6), + (3, 4, 0, 1, 2, 8, 9, 5, 6, 7), + (4, 0, 1, 2, 3, 9, 5, 6, 7, 8), + (5, 9, 8, 7, 6, 0, 4, 3, 2, 1), + (6, 5, 9, 8, 7, 1, 0, 4, 3, 2), + (7, 6, 5, 9, 8, 2, 1, 0, 4, 3), + (8, 7, 6, 5, 9, 3, 2, 1, 0, 4), + (9, 8, 7, 6, 5, 4, 3, 2, 1, 0)) +verhoeff_permutation_table = ( + (0, 1, 2, 3, 4, 5, 6, 7, 8, 9), + (1, 5, 7, 6, 2, 8, 3, 0, 9, 4), + (5, 8, 0, 3, 7, 9, 6, 1, 4, 2), + (8, 9, 1, 6, 0, 4, 3, 5, 2, 7), + (9, 4, 5, 3, 1, 2, 6, 8, 7, 0), + (4, 2, 8, 6, 5, 7, 3, 9, 0, 1), + (2, 7, 9, 3, 8, 0, 6, 4, 1, 5), + (7, 0, 4, 6, 9, 1, 3, 2, 5, 8)) + + +class VerhoeffAlgorithm(object): + def __init__(self): + pass + + @staticmethod + def verhoeff_checksum(number): + """Calculate the Verhoeff checksum over the provided number. The checksum + is returned as an int. Valid numbers should have a checksum of 0.""" + # transform number list + number = tuple(int(n) for n in reversed(str(number))) + # calculate checksum + check = 0 + for i, n in enumerate(number): + check = verhoeff_multiplication_table[check][verhoeff_permutation_table[i % 8][n]] + return check + + @staticmethod + def is_valid_verhoeff(number): + return VerhoeffAlgorithm.verhoeff_checksum(number) == 0 + + @staticmethod + def calculate_verhoeff_check_sum(number): + return str(verhoeff_multiplication_table[VerhoeffAlgorithm.verhoeff_checksum(str(number) + '0')].index(0)) diff --git a/smash/web/forms/__init__.py b/smash/web/forms/__init__.py index 41432b74e56c6ac250f4f182d9654a39f6f7f772..23d44c9490bb74edddc5ec5ad5d436dffd829ae2 100644 --- a/smash/web/forms/__init__.py +++ b/smash/web/forms/__init__.py @@ -4,8 +4,10 @@ from forms import WorkerAddForm, \ AvailabilityEditForm, HolidayAddForm from study_subject_forms import StudySubjectAddForm, StudySubjectDetailForm, StudySubjectEditForm from subject_forms import SubjectAddForm, SubjectEditForm, SubjectDetailForm +from voucher_forms import VoucherTypeForm, VoucherTypePriceForm, VoucherForm __all__ = [StudySubjectAddForm, StudySubjectDetailForm, StudySubjectEditForm, WorkerAddForm, WorkerEditForm, AppointmentDetailForm, AppointmentEditForm, AppointmentAddForm, VisitDetailForm, VisitAddForm, ContactAttemptForm, ContactAttemptEditForm, KitRequestForm, StatisticsForm, AvailabilityAddForm, - AvailabilityEditForm, HolidayAddForm, SubjectAddForm, SubjectEditForm, SubjectDetailForm] + AvailabilityEditForm, HolidayAddForm, SubjectAddForm, SubjectEditForm, SubjectDetailForm, VoucherTypeForm, + VoucherTypePriceForm, VoucherForm] diff --git a/smash/web/forms/forms.py b/smash/web/forms/forms.py index 570db43352dd9935a0ec0f9dc7871d6aaae6c427..e59536a3c6d8e6b7866d6a593f80ffcbc3bed321 100644 --- a/smash/web/forms/forms.py +++ b/smash/web/forms/forms.py @@ -7,7 +7,7 @@ from django.forms import ModelForm, Form from django.utils.dates import MONTHS from web.models import StudySubject, Worker, Appointment, Visit, AppointmentType, ContactAttempt, AppointmentTypeLink, \ - Availability, Holiday, VoucherType, VoucherTypePrice + Availability, Holiday from web.models.constants import SUBJECT_TYPE_CHOICES from web.views.notifications import get_filter_locations @@ -387,20 +387,3 @@ class HolidayAddForm(ModelForm): validate_availability_conflict(self, self.cleaned_data, availability) -class VoucherTypeForm(ModelForm): - class Meta: - model = VoucherType - exclude = ['study'] - - -class VoucherTypePriceForm(ModelForm): - start_date = forms.DateField(label="Start date", - widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d") - ) - end_date = forms.DateField(label="End date", - widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d") - ) - - class Meta: - model = VoucherTypePrice - exclude = ['voucher_type'] diff --git a/smash/web/forms/subject_forms.py b/smash/web/forms/subject_forms.py index cc717842d2065b99fb9e622ebc63ae0ec17fe785..05fe533ea79251c871e1a6c9b66454384758e437 100644 --- a/smash/web/forms/subject_forms.py +++ b/smash/web/forms/subject_forms.py @@ -3,6 +3,7 @@ import logging from django import forms from django.forms import ModelForm +from web.algorithm import VerhoeffAlgorithm, LuhnAlgorithm from web.forms.forms import DATEPICKER_DATE_ATTRS from web.models import Subject from web.models.constants import COUNTRY_OTHER_ID @@ -26,75 +27,14 @@ def is_valid_social_security_number(number): return False if not number.isdigit(): return False - if not is_luhn_valid(number[:12]): + if not LuhnAlgorithm.is_luhn_valid(number[:12]): return False - if not is_valid_verhoeff(number[:11] + number[12]): + if not VerhoeffAlgorithm.is_valid_verhoeff(number[:11] + number[12]): return False return True -def luhn_checksum(card_number): - def digits_of(n): - return [int(d) for d in str(n)] - - digits = digits_of(card_number) - odd_digits = digits[-1::-2] - even_digits = digits[-2::-2] - checksum = 0 - checksum += sum(odd_digits) - for d in even_digits: - checksum += sum(digits_of(d * 2)) - return checksum % 10 - - -def is_luhn_valid(card_number): - return luhn_checksum(card_number) == 0 - - -verhoeff_multiplication_table = ( - (0, 1, 2, 3, 4, 5, 6, 7, 8, 9), - (1, 2, 3, 4, 0, 6, 7, 8, 9, 5), - (2, 3, 4, 0, 1, 7, 8, 9, 5, 6), - (3, 4, 0, 1, 2, 8, 9, 5, 6, 7), - (4, 0, 1, 2, 3, 9, 5, 6, 7, 8), - (5, 9, 8, 7, 6, 0, 4, 3, 2, 1), - (6, 5, 9, 8, 7, 1, 0, 4, 3, 2), - (7, 6, 5, 9, 8, 2, 1, 0, 4, 3), - (8, 7, 6, 5, 9, 3, 2, 1, 0, 4), - (9, 8, 7, 6, 5, 4, 3, 2, 1, 0)) - -verhoeff_permutation_table = ( - (0, 1, 2, 3, 4, 5, 6, 7, 8, 9), - (1, 5, 7, 6, 2, 8, 3, 0, 9, 4), - (5, 8, 0, 3, 7, 9, 6, 1, 4, 2), - (8, 9, 1, 6, 0, 4, 3, 5, 2, 7), - (9, 4, 5, 3, 1, 2, 6, 8, 7, 0), - (4, 2, 8, 6, 5, 7, 3, 9, 0, 1), - (2, 7, 9, 3, 8, 0, 6, 4, 1, 5), - (7, 0, 4, 6, 9, 1, 3, 2, 5, 8)) - - -def verhoeff_checksum(number): - """Calculate the Verhoeff checksum over the provided number. The checksum - is returned as an int. Valid numbers should have a checksum of 0.""" - # transform number list - number = tuple(int(n) for n in reversed(str(number))) - # calculate checksum - check = 0 - for i, n in enumerate(number): - check = verhoeff_multiplication_table[check][verhoeff_permutation_table[i % 8][n]] - return check - - -def is_valid_verhoeff(number): - return verhoeff_checksum(number) == 0 - - -def calculate_verhoeff_check_sum(number): - return str(verhoeff_multiplication_table[verhoeff_checksum(str(number) + '0')].index(0)) - - FIELD_ORDER = ["first_name", "last_name", "sex", "date_born", "social_security_number", "default_written_communication_language", "languages", "phone_number", "phone_number_2", "phone_number_3", "address", "city", "postal_code", "country"] diff --git a/smash/web/forms/voucher_forms.py b/smash/web/forms/voucher_forms.py new file mode 100644 index 0000000000000000000000000000000000000000..68de4581885be709aca5cc71916a63c7d073ec63 --- /dev/null +++ b/smash/web/forms/voucher_forms.py @@ -0,0 +1,77 @@ +import datetime +import logging + +from django import forms +from django.forms import ModelForm +from django.utils import timezone + +from web.algorithm import VerhoeffAlgorithm +from web.forms.forms import DATEPICKER_DATE_ATTRS +from web.models import VoucherType, VoucherTypePrice, Voucher +from web.models.constants import VOUCHER_STATUS_NEW, VOUCHER_STATUS_USED + +logger = logging.getLogger(__name__) + + +class VoucherTypeForm(ModelForm): + class Meta: + model = VoucherType + exclude = ['study'] + + +class VoucherTypePriceForm(ModelForm): + start_date = forms.DateField(label="Start date", + widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d") + ) + end_date = forms.DateField(label="End date", + widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d") + ) + + class Meta: + model = VoucherTypePrice + exclude = ['voucher_type'] + + +class VoucherForm(ModelForm): + class Meta: + model = Voucher + fields = '__all__' + + def __init__(self, *args, **kwargs): + super(VoucherForm, self).__init__(*args, **kwargs) + self.fields['number'].widget.attrs['readonly'] = True + self.fields['number'].required = False + + self.fields['issue_date'].widget.attrs['readonly'] = True + self.fields['issue_date'].required = False + self.fields['expiry_date'].widget.attrs['readonly'] = True + self.fields['expiry_date'].required = False + self.fields['use_date'].widget.attrs['readonly'] = True + instance = getattr(self, 'instance', None) + if instance and instance.pk: + self.fields['voucher_type'].widget.attrs['readonly'] = True + if instance.status != VOUCHER_STATUS_NEW: + self.fields['status'].widget.attrs['readonly'] = True + self.fields['feedback'].widget.attrs['readonly'] = True + self.fields['usage_partner'].widget.attrs['readonly'] = True + + def clean(self): + if self.cleaned_data["status"] == VOUCHER_STATUS_USED and not self.cleaned_data["usage_partner"]: + self.add_error('usage_partner', "Partner must be defined for used voucher") + if self.cleaned_data["status"] != VOUCHER_STATUS_USED and self.cleaned_data["usage_partner"]: + self.add_error('status', "Status must be used for voucher with defined partner") + + def save(self, commit=True): + instance = super(VoucherForm, self).save(commit=False) + if not instance.id: + instance.issue_date = timezone.now() + instance.expiry_date = instance.issue_date + datetime.timedelta(days=92) + max_id = str(0).zfill(5) + if Voucher.objects.all().count() > 0: + max_id = str(Voucher.objects.latest('id').id).zfill(5) + instance.number = max_id + VerhoeffAlgorithm.calculate_verhoeff_check_sum(max_id) + if instance.status == VOUCHER_STATUS_USED and not instance.use_date: + instance.use_date = timezone.now() + + if commit: + instance.save() diff --git a/smash/web/migrations/0091_auto_20171208_1312.py b/smash/web/migrations/0091_auto_20171208_1312.py new file mode 100644 index 0000000000000000000000000000000000000000..3c823e288d5e639c074ca55b08041f45aa18d1c7 --- /dev/null +++ b/smash/web/migrations/0091_auto_20171208_1312.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-08 13:12 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0090_vouchertype_vouchertypeprice'), + ] + + operations = [ + migrations.AlterField( + model_name='vouchertypeprice', + name='voucher_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prices', to='web.VoucherType'), + ), + ] diff --git a/smash/web/migrations/0092_voucher.py b/smash/web/migrations/0092_voucher.py new file mode 100644 index 0000000000000000000000000000000000000000..963120ee80b966459eebaf0c9490d56b9ca37a85 --- /dev/null +++ b/smash/web/migrations/0092_voucher.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-08 15:00 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0091_auto_20171208_1312'), + ] + + operations = [ + migrations.CreateModel( + name='Voucher', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('number', models.CharField(max_length=10, unique=True, verbose_name=b'Number')), + ('issue_date', models.DateField(verbose_name=b'Issue date')), + ('expiry_date', models.DateField(verbose_name=b'Expiry date')), + ('use_date', models.DateField(verbose_name=b'Use date')), + ('status', models.CharField(choices=[(b'NEW', b'New'), (b'USED', b'Used'), (b'EXPIRED', b'Expired')], default=b'NEW', max_length=20, verbose_name=b'Status')), + ('feedback', models.TextField(blank=True, max_length=2000, verbose_name=b'Feedback')), + ('study_subject', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='web.StudySubject')), + ('usage_partner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.Worker')), + ('voucher_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.VoucherType')), + ], + ), + ] diff --git a/smash/web/migrations/0093_auto_20171208_1508.py b/smash/web/migrations/0093_auto_20171208_1508.py new file mode 100644 index 0000000000000000000000000000000000000000..839c2936a2f41311e6f79358abe979c4abe41b83 --- /dev/null +++ b/smash/web/migrations/0093_auto_20171208_1508.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-08 15:08 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0092_voucher'), + ] + + operations = [ + migrations.AlterField( + model_name='voucher', + name='usage_partner', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='web.Worker'), + ), + ] diff --git a/smash/web/migrations/0094_auto_20171208_1508.py b/smash/web/migrations/0094_auto_20171208_1508.py new file mode 100644 index 0000000000000000000000000000000000000000..5ca263649479e21e768968f07d8db7ba7d3d65ee --- /dev/null +++ b/smash/web/migrations/0094_auto_20171208_1508.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-08 15:08 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0093_auto_20171208_1508'), + ] + + operations = [ + migrations.AlterField( + model_name='voucher', + name='usage_partner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='web.Worker'), + ), + ] diff --git a/smash/web/migrations/0095_auto_20171208_1509.py b/smash/web/migrations/0095_auto_20171208_1509.py new file mode 100644 index 0000000000000000000000000000000000000000..37b50905594f0b4d2e34c07991813bb36b5fe7a9 --- /dev/null +++ b/smash/web/migrations/0095_auto_20171208_1509.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-08 15:09 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0094_auto_20171208_1508'), + ] + + operations = [ + migrations.AlterField( + model_name='voucher', + name='use_date', + field=models.DateField(null=True, verbose_name=b'Use date'), + ), + ] diff --git a/smash/web/migrations/0096_auto_20171208_1509.py b/smash/web/migrations/0096_auto_20171208_1509.py new file mode 100644 index 0000000000000000000000000000000000000000..b076d5c3f0bcb912bab2db36a94f67bc15746c85 --- /dev/null +++ b/smash/web/migrations/0096_auto_20171208_1509.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-08 15:09 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0095_auto_20171208_1509'), + ] + + operations = [ + migrations.AlterField( + model_name='voucher', + name='use_date', + field=models.DateField(blank=True, null=True, verbose_name=b'Use date'), + ), + ] diff --git a/smash/web/models/__init__.py b/smash/web/models/__init__.py index 6b9eac7eed601cc8b85949cb752d87ecce2f2764..77a30db094dfd4b977b2ca164a0070b39ea1e8cc 100644 --- a/smash/web/models/__init__.py +++ b/smash/web/models/__init__.py @@ -27,6 +27,7 @@ from item import Item from language import Language from subject import Subject from study_subject import StudySubject +from voucher import Voucher from study_subject_list import StudySubjectList from study_visit_list import StudyVisitList from appointment_list import AppointmentList @@ -38,5 +39,5 @@ from inconsistent_subject import InconsistentSubject, InconsistentField __all__ = [Study, FlyingTeam, Appointment, AppointmentType, Availability, Holiday, Item, Language, Location, Room, Subject, StudySubject, StudySubjectList, SubjectColumns, StudyNotificationParameters, AppointmentList, AppointmentColumns, Visit, Worker, ContactAttempt, ConfigurationItem, MailTemplate, - AppointmentTypeLink, VoucherType, VoucherTypePrice, + AppointmentTypeLink, VoucherType, VoucherTypePrice, Voucher, MissingSubject, InconsistentSubject, InconsistentField, Country, StudyColumns, VisitColumns, StudyVisitList] diff --git a/smash/web/models/constants.py b/smash/web/models/constants.py index fe6ade3b9b0c61fb9df46206f7c427d0735157f9..57794ce9ed36fd7287d221e1b64add2bdfeab395 100644 --- a/smash/web/models/constants.py +++ b/smash/web/models/constants.py @@ -81,3 +81,12 @@ COUNTRY_AFGHANISTAN_ID = 2 # id of the singleton Study, # TODO remove after allowing many studies per Smasch instance GLOBAL_STUDY_ID = 1 + +VOUCHER_STATUS_NEW = "NEW" +VOUCHER_STATUS_USED = "USED" +VOUCHER_STATUS_EXPIRED = "EXPIRED" +VOUCHER_STATUS_CHOICES = ( + (VOUCHER_STATUS_NEW, 'New'), + (VOUCHER_STATUS_USED, 'Used'), + (VOUCHER_STATUS_EXPIRED, 'Expired'), +) diff --git a/smash/web/models/voucher.py b/smash/web/models/voucher.py new file mode 100644 index 0000000000000000000000000000000000000000..f7cc8f15c50c5c84cec75c33483678f4abab4ba0 --- /dev/null +++ b/smash/web/models/voucher.py @@ -0,0 +1,58 @@ +# coding=utf-8 + +from django.db import models + +from web.models import VoucherType, StudySubject, Worker +from web.models.constants import VOUCHER_STATUS_CHOICES, VOUCHER_STATUS_NEW + + +class Voucher(models.Model): + class Meta: + app_label = 'web' + + number = models.CharField( + max_length=10, + verbose_name='Number', + blank=False, + null=False, + unique=True + ) + + issue_date = models.DateField(verbose_name='Issue date', null=False) + expiry_date = models.DateField(verbose_name='Expiry date', null=False) + use_date = models.DateField(verbose_name='Use date', null=True, blank=True) + voucher_type = models.ForeignKey( + VoucherType, + on_delete=models.CASCADE, + null=False, + ) + + study_subject = models.ForeignKey( + StudySubject, + on_delete=models.CASCADE, + null=False, + editable=False + ) + + status = models.CharField(max_length=20, choices=VOUCHER_STATUS_CHOICES, + verbose_name='Status', + default=VOUCHER_STATUS_NEW + ) + + feedback = models.TextField(max_length=2000, + blank=True, + verbose_name='Feedback' + ) + + usage_partner = models.ForeignKey( + Worker, + on_delete=models.CASCADE, + null=True, + blank=True + ) + + def __str__(self): + return "%s - %s %s" % (self.number, self.study_subject.subject.first_name, self.study_subject.subject.last_name) + + def __unicode__(self): + return "%s - %s %s" % (self.number, self.study_subject.subject.first_name, self.study_subject.subject.last_name) diff --git a/smash/web/templates/sidebar.html b/smash/web/templates/sidebar.html index a8dd1d750c13fb5c824c11c40f9d9681bf535126..71c325a1cc768df50f5898b59b8e3e1b1aeefa6a 100644 --- a/smash/web/templates/sidebar.html +++ b/smash/web/templates/sidebar.html @@ -57,6 +57,14 @@ <span>Export</span> </a> </li> + + <li data-desc="vouchers"> + <a href="{% url 'web.views.vouchers' %}"> + <i class="fa fa-user-md"></i> + <span>Vouchers</span> + </a> + </li> + <li data-desc="configuration" class="treeview"> <a href="#"> <i class="fa fa-wrench"></i> <span>Configuration</span> diff --git a/smash/web/templates/vouchers/add.html b/smash/web/templates/vouchers/add.html new file mode 100644 index 0000000000000000000000000000000000000000..994ab3cb9337086474d330f2b288d6a4aaa2bba5 --- /dev/null +++ b/smash/web/templates/vouchers/add.html @@ -0,0 +1,9 @@ +{% extends "vouchers/add_edit.html" %} + +{% block page_header %}New voucher{% endblock page_header %} + +{% block title %}{{ block.super }} - Add voucher{% endblock %} + +{% block form-title %}Enter voucher details{% endblock %} + +{% block save-button %}Add{% endblock %} diff --git a/smash/web/templates/vouchers/add_edit.html b/smash/web/templates/vouchers/add_edit.html new file mode 100644 index 0000000000000000000000000000000000000000..8e5e5dfbaf497bf4a88f70ded534fd2e8ef92a8f --- /dev/null +++ b/smash/web/templates/vouchers/add_edit.html @@ -0,0 +1,77 @@ +{% extends "_base.html" %} +{% load static %} +{% load filters %} + +{% block styles %} + {{ block.super }} + <link rel="stylesheet" href="{% static 'AdminLTE/plugins/awesomplete/awesomplete.css' %}"/> + +{% endblock styles %} + +{% block ui_active_tab %}'configuration'{% endblock ui_active_tab %} +{% block page_description %}{% endblock page_description %} + +{% block breadcrumb %} + {% include "vouchers/breadcrumb.html" %} +{% endblock breadcrumb %} + +{% block maincontent %} + + {% block content %} + <div class="row"> + <div class="col-md-12"> + <div class="box box-success"> + <div class="box-header with-border"> + <h3 class="box-title">{% block form-title %}Enter voucher details{% endblock %}</h3> + </div> + + + <form method="post" action="" class="form-horizontal" enctype="multipart/form-data"> + {% csrf_token %} + + <div class="box-body"> + {% for field in form %} + <div class="form-group {% if field.errors %}has-error{% endif %}"> + <label class="col-sm-4 col-lg-offset-1 col-lg-2 control-label"> + {{ field.label }} + </label> + + <div class="col-sm-8 col-lg-4"> + {{ field|add_class:'form-control' }} + {% if field.errors %} + <span class="help-block">{{ field.errors }}</span> + {% endif %} + </div> + + + </div> + {% endfor %} + </div><!-- /.box-body --> + <div class="box-footer"> + <div class="col-sm-6"> + <button type="submit" class="btn btn-block btn-success">{% block save-button %} + Add{% endblock %} + </button> + </div> + <div class="col-sm-6"> + <a href="{% url 'web.views.vouchers' %}" + class="btn btn-block btn-default">Cancel</a> + </div> + </div><!-- /.box-footer --> + </form> + </div> + + </div> + </div> + + {% endblock %} + + +{% endblock maincontent %} + +{% block scripts %} + {{ block.super }} + + <script src="{% static 'AdminLTE/plugins/awesomplete/awesomplete.min.js' %}"></script> + +{% endblock scripts %} \ No newline at end of file diff --git a/smash/web/templates/vouchers/breadcrumb.html b/smash/web/templates/vouchers/breadcrumb.html new file mode 100644 index 0000000000000000000000000000000000000000..f71acc808f35010d4240224a41f00db70a830f0d --- /dev/null +++ b/smash/web/templates/vouchers/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.vouchers' %}">Vouchers</a></li> \ No newline at end of file diff --git a/smash/web/templates/vouchers/edit.html b/smash/web/templates/vouchers/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..20e1bea665a6be2c70b8f260a23d8fee1e2b1ba5 --- /dev/null +++ b/smash/web/templates/vouchers/edit.html @@ -0,0 +1,10 @@ +{% extends "vouchers/add_edit.html" %} + +{% block page_header %}Edit voucher "{{ voucher.number }}"{% endblock page_header %} + +{% block title %}{{ block.super }} - Edit voucher "{{ voucher.number }}"{% endblock %} + +{% block form-title %}Enter voucher details{% endblock %} + +{% block save-button %}Save{% endblock %} + diff --git a/smash/web/templates/vouchers/list.html b/smash/web/templates/vouchers/list.html new file mode 100644 index 0000000000000000000000000000000000000000..7a29b16f449daa5efa1baf27c23dc3d98d17c69e --- /dev/null +++ b/smash/web/templates/vouchers/list.html @@ -0,0 +1,74 @@ +{% extends "_base.html" %} +{% load static %} + +{% block styles %} + {{ block.super }} + <!-- DataTables --> + <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}"> +{% endblock styles %} + +{% block ui_active_tab %}'configuration'{% endblock ui_active_tab %} +{% block page_header %}Vouchers{% endblock page_header %} +{% block page_description %}{% endblock page_description %} + +{% block breadcrumb %} + {% include "vouchers/breadcrumb.html" %} +{% endblock breadcrumb %} + +{% block maincontent %} + + <div class="box-body"> + <table id="table" class="table table-bordered table-striped"> + <thead> + <tr> + <th>Number</th> + <th>First name</th> + <th>Last name</th> + <th>Issue date</th> + <th>Expiry date</th> + <th>Status</th> + <th>Use date</th> + <th>Partner</th> + <th>Feedback</th> + <th>Edit</th> + </tr> + </thead> + <tbody> + {% for voucher in vouchers %} + <tr> + <td>{{ voucher.number }}</td> + <td>{{ voucher.study_subject.subject.first_name }}</td> + <td>{{ voucher.study_subject.subject.last_name }}</td> + <td>{{ voucher.issue_date }}</td> + <td>{{ voucher.expiry_date }}</td> + <td>{{ voucher.status }}</td> + <td>{{ voucher.use_date }}</td> + <td>{{ voucher.usage_partner.first_name }} {{ voucher.usage_partner.last_name }}</td> + <td>{{ voucher.feedback }}</td> + <td><a href="{% url 'web.views.voucher_edit' voucher.id %}"><i class="fa fa-edit"></i></a></td> + </tr> + {% endfor %} + </tbody> + </table> + </div> +{% endblock maincontent %} + +{% block scripts %} + {{ block.super }} + + <script src="{% static 'AdminLTE/plugins/datatables/jquery.dataTables.min.js' %}"></script> + <script src="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.min.js' %}"></script> + + <script> + $(function () { + $('#table').DataTable({ + "paging": true, + "lengthChange": false, + "searching": true, + "ordering": true, + "info": true, + "autoWidth": false + }); + }); + </script> +{% endblock scripts %} diff --git a/smash/web/tests/__init__.py b/smash/web/tests/__init__.py index 4d84e29961f15842b84f3f3c8878073c755a0cbe..692232677b9701a19afaa9b63a85628248f09e5f 100644 --- a/smash/web/tests/__init__.py +++ b/smash/web/tests/__init__.py @@ -1,3 +1,4 @@ +import logging import os from django.conf import settings from django.contrib.auth.models import User @@ -8,6 +9,8 @@ from functions import create_worker settings.MEDIA_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') +logger = logging.getLogger(__name__) + class LoggedInTestCase(TestCase): def setUp(self): diff --git a/smash/web/tests/forms/test_subject_forms.py b/smash/web/tests/forms/test_subject_forms.py index beb2bcbd1acff6c20ff7dae0ca213588d600b6bb..fe109455c9595a3b3ee475a27ff23509e41a2e1b 100644 --- a/smash/web/tests/forms/test_subject_forms.py +++ b/smash/web/tests/forms/test_subject_forms.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) class StudySubjectAddFormTests(LoggedInWithWorkerTestCase): def setUp(self): - super(LoggedInWithWorkerTestCase, self).setUp() + super(StudySubjectAddFormTests, self).setUp() self.subject = create_subject() def test_is_valid_social_security_number_too_short(self): diff --git a/smash/web/tests/forms/test_voucher_forms.py b/smash/web/tests/forms/test_voucher_forms.py new file mode 100644 index 0000000000000000000000000000000000000000..e7310f34cc85ce677cec636da9be9de5fdead66b --- /dev/null +++ b/smash/web/tests/forms/test_voucher_forms.py @@ -0,0 +1,71 @@ +import logging + +from django.urls import reverse + +from web.forms import VoucherForm +from web.models import Voucher +from web.models.constants import VOUCHER_STATUS_USED +from web.tests import LoggedInWithWorkerTestCase +from web.tests.functions import create_study_subject, create_voucher_type, format_form_field, create_voucher + +logger = logging.getLogger(__name__) + + +class VoucherFormTests(LoggedInWithWorkerTestCase): + def setUp(self): + super(VoucherFormTests, self).setUp() + + def test_auto_generated_use_date(self): + study_subject = create_study_subject() + create_voucher(study_subject) + + voucher_form = VoucherForm() + form_data = { + "status": VOUCHER_STATUS_USED, + "usage_partner": str(self.worker.id), + "voucher_type": create_voucher_type().id + } + for key, value in voucher_form.initial.items(): + form_data[key] = format_form_field(value) + + url = reverse('web.views.voucher_add') + '?study_subject_id=' + str(study_subject.id) + response = self.client.post(url, data=form_data) + self.assertEqual(response.status_code, 302) + + self.assertEqual(2, Voucher.objects.all().count()) + self.assertEqual(1, Voucher.objects.filter(use_date__isnull=False).count()) + + def test_valid_usage_partner(self): + study_subject = create_study_subject() + voucher = create_voucher(study_subject) + voucher.status = VOUCHER_STATUS_USED + voucher.save() + + voucher_form = VoucherForm(instance=voucher) + + form_data = {} + for key, value in voucher_form.initial.items(): + form_data[key] = format_form_field(value) + form_data["usage_partner"] = "" + + voucher_form = VoucherForm(instance=voucher, data=form_data) + + self.assertFalse(voucher_form.is_valid()) + self.assertTrue("usage_partner" in voucher_form.errors) + + def test_valid_status(self): + study_subject = create_study_subject() + voucher = create_voucher(study_subject) + voucher.usage_partner = self.worker + voucher.save() + + voucher_form = VoucherForm(instance=voucher) + + form_data = {} + for key, value in voucher_form.initial.items(): + form_data[key] = format_form_field(value) + + voucher_form = VoucherForm(instance=voucher, data=form_data) + + self.assertFalse(voucher_form.is_valid()) + self.assertTrue("status" in voucher_form.errors) diff --git a/smash/web/tests/functions.py b/smash/web/tests/functions.py index 52ba55dfdfa75cfec5161885500b5294244371a4..ad3da907a16f158491a12f5f85a905274d1d3064 100644 --- a/smash/web/tests/functions.py +++ b/smash/web/tests/functions.py @@ -6,10 +6,10 @@ from django.contrib.auth.models import User from web.models import Location, AppointmentType, StudySubject, Worker, Visit, Appointment, ConfigurationItem, \ Language, ContactAttempt, FlyingTeam, Availability, Subject, Study, StudyColumns, StudyNotificationParameters, \ - VoucherType, VoucherTypePrice + VoucherType, VoucherTypePrice, Voucher from web.models.constants import REDCAP_TOKEN_CONFIGURATION_TYPE, REDCAP_BASE_URL_CONFIGURATION_TYPE, \ SEX_CHOICES_MALE, SUBJECT_TYPE_CHOICES_CONTROL, CONTACT_TYPES_PHONE, \ - MONDAY_AS_DAY_OF_WEEK, COUNTRY_AFGHANISTAN_ID + MONDAY_AS_DAY_OF_WEEK, COUNTRY_AFGHANISTAN_ID, VOUCHER_STATUS_NEW from web.redcap_connector import RedcapSubject from web.views.notifications import get_today_midnight_date @@ -67,7 +67,18 @@ def create_study(name="test"): return Study.objects.create(name=name, columns=study_columns, notification_parameters=notification_parameters) -def create_empty_notification_parametres(): +def create_voucher(study_subject=None): + if study_subject is None: + study_subject = create_study_subject() + return Voucher.objects.create(number="123456", + study_subject=study_subject, + issue_date=get_today_midnight_date(), + expiry_date=get_today_midnight_date(), + voucher_type=create_voucher_type(), + status = VOUCHER_STATUS_NEW) + + +def create_empty_notification_parameters(): return StudyNotificationParameters.objects.create( exceeded_visits_visible=False, unfinished_visits_visible=False, @@ -84,7 +95,7 @@ def create_empty_notification_parametres(): def create_empty_study(name="test"): study_columns = create_empty_study_columns() - notification_parameters = create_empty_notification_parametres() + notification_parameters = create_empty_notification_parameters() result = create_study(name) result.columns = study_columns result.notification_parameters = notification_parameters diff --git a/smash/web/tests/models/test_voucher.py b/smash/web/tests/models/test_voucher.py new file mode 100644 index 0000000000000000000000000000000000000000..ec5e94316c2bda04dd70bb337498063ff4decca8 --- /dev/null +++ b/smash/web/tests/models/test_voucher.py @@ -0,0 +1,15 @@ +import logging + +from django.test import TestCase + +from web.tests.functions import create_voucher + +logger = logging.getLogger(__name__) + + +class VoucherTests(TestCase): + def test_to_string(self): + voucher = create_voucher() + + self.assertTrue(voucher.number in str(voucher)) + self.assertTrue(voucher.number in unicode(voucher)) diff --git a/smash/web/tests/models/test_voucher_type.py b/smash/web/tests/models/test_voucher_type.py new file mode 100644 index 0000000000000000000000000000000000000000..3c47e30bc18f79f2f41ae75cdb6fac9861b6a5f1 --- /dev/null +++ b/smash/web/tests/models/test_voucher_type.py @@ -0,0 +1,15 @@ +import logging + +from django.test import TestCase + +from web.tests.functions import create_voucher_type + +logger = logging.getLogger(__name__) + + +class VoucherTypeTests(TestCase): + def test_to_string(self): + voucher_type = create_voucher_type() + + self.assertTrue(voucher_type.code in str(voucher_type)) + self.assertTrue(voucher_type.code in unicode(voucher_type)) diff --git a/smash/web/tests/view/test_notifications.py b/smash/web/tests/view/test_notifications.py index e02b515089640176d13e3b57c13c0b7a902a64b8..e84952c33c2207af6c1c65c0e505da4768e0d876 100644 --- a/smash/web/tests/view/test_notifications.py +++ b/smash/web/tests/view/test_notifications.py @@ -7,7 +7,7 @@ from web.models import Appointment, Location, AppointmentTypeLink, Study, Visit from web.models.constants import GLOBAL_STUDY_ID from web.tests import LoggedInTestCase from web.tests.functions import create_appointment, create_location, create_worker, create_appointment_type, \ - create_empty_notification_parametres, create_study_subject, create_visit + create_empty_notification_parameters, create_study_subject, create_visit from web.views.notifications import \ get_approaching_visits_for_mail_contact, \ get_approaching_visits_for_mail_contact_count, \ @@ -118,7 +118,7 @@ class NotificationViewTests(LoggedInTestCase): def test_get_notifications_with_empty_study_notification(self): study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] - study.notification_parameters = create_empty_notification_parametres() + study.notification_parameters = create_empty_notification_parameters() study.save() create_worker(self.user) diff --git a/smash/web/tests/view/test_voucher.py b/smash/web/tests/view/test_voucher.py new file mode 100644 index 0000000000000000000000000000000000000000..871bf563689ad15dfc4d97c2d1cb1ebd28b6b4a4 --- /dev/null +++ b/smash/web/tests/view/test_voucher.py @@ -0,0 +1,54 @@ +import logging + +from django.urls import reverse + +from web.forms import VoucherForm +from web.models import Voucher +from web.models.constants import VOUCHER_STATUS_NEW +from web.tests.functions import create_voucher, create_study_subject, format_form_field, create_voucher_type +from .. import LoggedInTestCase + +logger = logging.getLogger(__name__) + + +class VoucherTypeViewTests(LoggedInTestCase): + def test_render_add_voucher_request(self): + response = self.client.get(reverse('web.views.voucher_add')) + self.assertEqual(response.status_code, 200) + + def test_render_edit_voucher_request(self): + voucher = create_voucher() + response = self.client.get(reverse('web.views.voucher_edit', kwargs={'pk': voucher.id})) + self.assertEqual(response.status_code, 200) + + def test_add_voucher(self): + study_subject = create_study_subject() + visit_detail_form = VoucherForm() + form_data = { + "status": VOUCHER_STATUS_NEW, + "voucher_type": create_voucher_type().id + } + for key, value in visit_detail_form.initial.items(): + form_data[key] = format_form_field(value) + + url = reverse('web.views.voucher_add') + '?study_subject_id=' + str(study_subject.id) + response = self.client.post(url, data=form_data) + self.assertEqual(response.status_code, 302) + + self.assertEqual(1, Voucher.objects.all().count()) + + def test_edit_voucher(self): + voucher = create_voucher() + voucher_form = VoucherForm(instance=voucher) + form_data = {} + for key, value in voucher_form.initial.items(): + form_data[key] = format_form_field(value) + + form_data["usage_partner"] = "" + form_data["use_date"] = "" + + url = reverse('web.views.voucher_edit', kwargs={'pk': voucher.id}) + response = self.client.post(url, data=form_data) + self.assertEqual(response.status_code, 302) + + self.assertEqual(1, Voucher.objects.all().count()) diff --git a/smash/web/tests/view/test_voucher_type_price.py b/smash/web/tests/view/test_voucher_type_price.py index be90a2fd10abf9ffe14a774be02eee329d1e4fab..5032c40b46ed966049086ef792ba09061bbf78ba 100644 --- a/smash/web/tests/view/test_voucher_type_price.py +++ b/smash/web/tests/view/test_voucher_type_price.py @@ -2,7 +2,7 @@ import logging from django.urls import reverse -from web.forms.forms import VoucherTypePriceForm +from web.forms import VoucherTypePriceForm from web.models import VoucherType, VoucherTypePrice from web.tests.functions import create_voucher_type, create_voucher_type_price, format_form_field from .. import LoggedInTestCase @@ -53,6 +53,6 @@ class VoucherTypePriceViewTests(LoggedInTestCase): url = reverse('web.views.voucher_type_price_edit', kwargs={'pk': voucher_type_price.id, 'voucher_type_id': voucher_type_price.voucher_type.id}) - response = self.client.post(url, data=form_data) + self.client.post(url, data=form_data) self.assertEqual(90.50, VoucherTypePrice.objects.get(id=voucher_type_price.id).price) diff --git a/smash/web/urls.py b/smash/web/urls.py index c2bc3d3bd480fecf6f3977d6c3ed209af04cdb34..1ee0abb3b5510f17ced9243d6cec7f7c15543f5a 100644 --- a/smash/web/urls.py +++ b/smash/web/urls.py @@ -178,6 +178,14 @@ urlpatterns = [ url(r'^voucher_types/(?P<voucher_type_id>\d+)/prices/(?P<pk>\d+)/edit$', views.voucher_type_price.VoucherTypePriceEditView.as_view(), name='web.views.voucher_type_price_edit'), + #################### + # VOUCHERS # + #################### + + url(r'^vouchers$', views.voucher.VoucherListView.as_view(), name='web.views.vouchers'), + url(r'^vouchers/add$', views.voucher.VoucherCreateView.as_view(), name='web.views.voucher_add'), + url(r'^vouchers/(?P<pk>\d+)/edit$', views.voucher.VoucherEditView.as_view(), name='web.views.voucher_edit'), + #################### # STATISTICS # #################### diff --git a/smash/web/views/__init__.py b/smash/web/views/__init__.py index 01bdb13fac46bb895c6e33ebc0b9a78f9089461d..9a8ed660a3ba8424b17d85a53d890c66c76cfd66 100644 --- a/smash/web/views/__init__.py +++ b/smash/web/views/__init__.py @@ -72,6 +72,7 @@ import export import contact_attempt import configuration_item import language +import voucher import voucher_type import voucher_type_price import redcap diff --git a/smash/web/views/voucher.py b/smash/web/views/voucher.py new file mode 100644 index 0000000000000000000000000000000000000000..840e5d34f67df75f44e8e01d9e97838977e6a51c --- /dev/null +++ b/smash/web/views/voucher.py @@ -0,0 +1,55 @@ +# coding=utf-8 +import logging + +from django.contrib.messages.views import SuccessMessageMixin +from django.urls import reverse_lazy +from django.views.generic import CreateView +from django.views.generic import ListView +from django.views.generic import UpdateView + +from web.forms import VoucherForm +from web.models import Voucher +from web.models.constants import GLOBAL_STUDY_ID +from . import WrappedView + +logger = logging.getLogger(__name__) + + +class VoucherListView(ListView, WrappedView): + model = Voucher + context_object_name = "vouchers" + template_name = 'vouchers/list.html' + + +class VoucherCreateView(CreateView, WrappedView): + form_class = VoucherForm + model = Voucher + + template_name = "vouchers/add.html" + success_url = reverse_lazy('web.views.vouchers') + success_message = "Voucher type created" + + def form_valid(self, form): + form.instance.study_id = GLOBAL_STUDY_ID + # noinspection PyUnresolvedReferences + form.instance.study_subject_id = self.request.GET.get("study_subject_id", -1) + return super(VoucherCreateView, self).form_valid(form) + + def get_success_url(self, **kwargs): + # noinspection PyUnresolvedReferences + return reverse_lazy('web.views.subject_edit', kwargs={'id': self.request.GET.get("study_subject_id", -1)}) + + +class VoucherEditView(SuccessMessageMixin, UpdateView, WrappedView): + form_class = VoucherForm + model = Voucher + + success_url = reverse_lazy('web.views.vouchers') + success_message = "Voucher saved successfully" + template_name = "vouchers/edit.html" + context_object_name = "voucher_type" + + def get_success_url(self, **kwargs): + # noinspection PyUnresolvedReferences + study_subject_id = Voucher.objects.get(id=self.kwargs['pk']).study_subject.id + return reverse_lazy('web.views.subject_edit', kwargs={'id': study_subject_id}) diff --git a/smash/web/views/voucher_type.py b/smash/web/views/voucher_type.py index 87b13059fb6b2812c22f8aab0e6a0d9e3bb06c29..5cb45eb57245bbf5df0e1163eb274266f30e871e 100644 --- a/smash/web/views/voucher_type.py +++ b/smash/web/views/voucher_type.py @@ -4,7 +4,7 @@ from django.views.generic import CreateView from django.views.generic import ListView from django.views.generic import UpdateView -from web.forms.forms import VoucherTypeForm +from web.forms import VoucherTypeForm from web.models import VoucherType from web.models.constants import GLOBAL_STUDY_ID from . import WrappedView diff --git a/smash/web/views/voucher_type_price.py b/smash/web/views/voucher_type_price.py index f93a045f3d188adc78d2994d5e5e6d135a77f8fc..11f6f4dd43981c64e1bb032accd8b543f763939a 100644 --- a/smash/web/views/voucher_type_price.py +++ b/smash/web/views/voucher_type_price.py @@ -5,7 +5,7 @@ from django.urls import reverse_lazy from django.views.generic import CreateView from django.views.generic import UpdateView -from web.forms.forms import VoucherTypePriceForm +from web.forms import VoucherTypePriceForm from web.models import VoucherTypePrice from . import WrappedView