diff --git a/.gitignore b/.gitignore index 7230e12f66c4c676e29c1c6a86a1fbaa7aaedd4c..83c08fba63f2be651b4dbee75798abfe6c694147 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ env/* # Folder with db statics (dev mode) smash/~/ +# files uploaded and hosted by django +smash/uploads/ +smash/smash/uploads/ + # Disable python bytecode *.pyc #vim swap files 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/api_views/serialization_utils.py b/smash/web/api_views/serialization_utils.py index c1e857ed889a0973ead5f9dd0a8c5e644c32527d..89e2c9c19c2eb5aa958a17165fe12d440b842c58 100644 --- a/smash/web/api_views/serialization_utils.py +++ b/smash/web/api_views/serialization_utils.py @@ -41,8 +41,8 @@ def serialize_datetime(date): def add_column(result, name, field_name, column_list, param, columns_used_in_study=None, visible_param=None, - sortable=True): - add = True + sortable=True, add_param=True): + add = add_param if columns_used_in_study: add = getattr(columns_used_in_study, field_name) if add: diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py index ac069955aae0681027e921742dbf110f7b85c4d5..589d19d633efa14f693aff4fbe624ec80c55c1b2 100644 --- a/smash/web/api_views/subject.py +++ b/smash/web/api_views/subject.py @@ -17,6 +17,7 @@ from web.views.notifications import get_subjects_with_no_visit, get_subjects_wit logger = logging.getLogger(__name__) +# noinspection PyUnusedLocal @login_required def cities(request): result_subjects = Subject.objects.filter(city__isnull=False).values_list('city').distinct() @@ -25,6 +26,7 @@ def cities(request): }) +# noinspection PyUnusedLocal @login_required def referrals(request): result_subjects = StudySubject.objects.filter(referral__isnull=False).values_list('referral').distinct() @@ -51,10 +53,17 @@ def get_subject_columns(request, subject_list_type): add_column(result, "Screening", "screening_number", study_subject_columns, "string_filter", study.columns) add_column(result, "First name", "first_name", subject_columns, "string_filter") add_column(result, "Last name", "last_name", subject_columns, "string_filter") + add_column(result, "Social Security Number", "social_security_number", subject_columns, "string_filter") add_column(result, "Date of birth", "date_born", subject_columns, None) add_column(result, "Contact on", "datetime_contact_reminder", study_subject_columns, None, study.columns) add_column(result, "Last contact attempt", "last_contact_attempt", study_subject_list, None) add_column(result, "Referred by", "referral", study_subject_columns, "string_filter", study.columns) + add_column(result, "Health partner name", "health_partner_first_name", None, "string_filter", + add_param=study.columns.health_partner, + visible_param=study_subject_columns.health_partner) + add_column(result, "Health partner last name", "health_partner_last_name", None, "string_filter", + add_param=study.columns.health_partner, + visible_param=study_subject_columns.health_partner) add_column(result, "Location", "default_location", study_subject_columns, "location_filter", study.columns) add_column(result, "Flying team location", "flying_team", study_subject_columns, "flying_team_filter", study.columns) @@ -116,6 +125,12 @@ def get_subjects_order(subjects_to_be_ordered, order_column, order_direction): result = subjects_to_be_ordered.order_by(order_direction + 'resigned') elif order_column == "information_sent": result = subjects_to_be_ordered.order_by(order_direction + 'information_sent') + elif order_column == "health_partner_first_name": + result = subjects_to_be_ordered.order_by(order_direction + 'health_partner__first_name') + elif order_column == "health_partner_last_name": + result = subjects_to_be_ordered.order_by(order_direction + 'health_partner__last_name') + elif order_column == "social_security_number": + result = subjects_to_be_ordered.order_by(order_direction + 'subject__social_security_number') elif order_column == "postponed": result = subjects_to_be_ordered.order_by(order_direction + 'postponed') elif order_column == "type": @@ -226,6 +241,12 @@ def get_subjects_filtered(subjects_to_be_filtered, filters): result = result.filter(postponed=(value == "true")) elif column == "information_sent": result = result.filter(information_sent=(value == "true")) + elif column == "health_partner_first_name": + result = result.filter(health_partner__first_name__icontains=value) + elif column == "health_partner_last_name": + result = result.filter(health_partner__last_name__icontains=value) + elif column == "social_security_number": + result = result.filter(subject__social_security_number__icontains=value) elif column == "default_location": result = result.filter(default_location=value) elif column == "flying_team": @@ -244,7 +265,6 @@ def get_subjects_filtered(subjects_to_be_filtered, filters): else: message += str(column) logger.warn(message) - return result @@ -289,6 +309,7 @@ def subjects(request, type): return e500_error(request) +# noinspection PyUnusedLocal @login_required def types(request): data = [{"id": subject_type_id, "name": subject_type_name} for subject_type_id, subject_type_name in @@ -338,6 +359,12 @@ def serialize_subject(study_subject): else: last_contact_attempt_string = "" + health_partner_first_name = "" + if study_subject.health_partner: + health_partner_first_name = study_subject.health_partner.first_name + health_partner_last_name = "" + if study_subject.health_partner: + health_partner_last_name = study_subject.health_partner.last_name result = { "first_name": study_subject.subject.first_name, "last_name": study_subject.subject.last_name, @@ -353,6 +380,9 @@ def serialize_subject(study_subject): "resigned": bool_to_yes_no(study_subject.resigned), "postponed": bool_to_yes_no(study_subject.postponed), "information_sent": bool_to_yes_no(study_subject.information_sent), + "health_partner_first_name": health_partner_first_name, + "health_partner_last_name": health_partner_last_name, + "social_security_number": study_subject.subject.social_security_number, "type": study_subject.get_type_display(), "id": study_subject.id, "visits": serialized_visits, 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 92088a283f2579b97eaa399a20937231ed855d74..1aa33b920dd8735547cc2b8c786b81ef79e369d6 100644 --- a/smash/web/forms/forms.py +++ b/smash/web/forms/forms.py @@ -400,20 +400,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/study_subject_forms.py b/smash/web/forms/study_subject_forms.py index aacd999186c4d76bd08a7545369d488530456558..94fa1fcfe15551d3d9510337db6ad1828158ca24 100644 --- a/smash/web/forms/study_subject_forms.py +++ b/smash/web/forms/study_subject_forms.py @@ -5,18 +5,31 @@ from django import forms from django.forms import ModelForm from web.forms.forms import DATETIMEPICKER_DATE_ATTRS, get_worker_from_args -from web.models import StudySubject, Study, StudyColumns +from web.models import StudySubject, Study, StudyColumns, VoucherType from web.models.constants import SCREENING_NUMBER_PREFIXES_FOR_TYPE +from web.widgets.secure_file_widget import SecuredFileWidget logger = logging.getLogger(__name__) -class StudySubjectAddForm(ModelForm): +class StudySubjectForm(ModelForm): datetime_contact_reminder = forms.DateTimeField(label="Contact on", widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS), required=False ) + referral_letter = forms.FileField(label='Select a file', widget=SecuredFileWidget(), required=False) + + voucher_types = forms.ModelMultipleChoiceField(required=False, + widget=forms.CheckboxSelectMultiple, + queryset=VoucherType.objects.all(), + ) + + def __init__(self, *args, **kwargs): + super(StudySubjectForm, self).__init__(*args, **kwargs) + + +class StudySubjectAddForm(StudySubjectForm): class Meta: model = StudySubject fields = '__all__' @@ -26,12 +39,12 @@ class StudySubjectAddForm(ModelForm): self.user = get_worker_from_args(kwargs) self.study = get_study_from_args(kwargs) - super(ModelForm, self).__init__(*args, **kwargs) + super(StudySubjectAddForm, self).__init__(*args, **kwargs) prepare_study_subject_fields(fields=self.fields, study=self.study) def save(self, commit=True): self.instance.study_id = self.study.id - return super(ModelForm, self).save(commit) + return super(StudySubjectAddForm, self).save(commit) def build_screening_number(self, cleaned_data): screening_number = cleaned_data.get('screening_number', None) @@ -83,7 +96,7 @@ def get_new_screening_number(screening_number_prefix): return screening_number_prefix + str(result_number + 1).zfill(3) -class StudySubjectDetailForm(ModelForm): +class StudySubjectDetailForm(StudySubjectForm): class Meta: model = StudySubject fields = '__all__' @@ -104,11 +117,7 @@ def get_study_from_study_subject_instance(study_subject): return Study(columns=StudyColumns()) -class StudySubjectEditForm(ModelForm): - datetime_contact_reminder = forms.DateTimeField(label="Contact on", - widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS), - required=False - ) +class StudySubjectEditForm(StudySubjectForm): def __init__(self, *args, **kwargs): was_resigned = kwargs.get('was_resigned', False) @@ -166,6 +175,12 @@ def prepare_study_subject_fields(fields, study): prepare_field(fields, study.columns, 'pd_in_family') prepare_field(fields, study.columns, 'resigned') prepare_field(fields, study.columns, 'resign_reason') + prepare_field(fields, study.columns, 'referral_letter') + prepare_field(fields, study.columns, 'health_partner') + prepare_field(fields, study.columns, 'health_partner_feedback_agreement') + prepare_field(fields, study.columns, 'screening') + prepare_field(fields, study.columns, 'previously_in_study') + prepare_field(fields, study.columns, 'voucher_types') def validate_subject_screening_number(self, cleaned_data): diff --git a/smash/web/forms/subject_forms.py b/smash/web/forms/subject_forms.py index b4f12f65c5eb51b4bf0f221517d04123fc49387e..05fe533ea79251c871e1a6c9b66454384758e437 100644 --- a/smash/web/forms/subject_forms.py +++ b/smash/web/forms/subject_forms.py @@ -1,16 +1,40 @@ +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 +logger = logging.getLogger(__name__) + def validate_subject_country(self, cleaned_data): if cleaned_data['country'].id == COUNTRY_OTHER_ID: self.add_error('country', "Select valid country") +def validate_social_security_number(self, number): + if not is_valid_social_security_number(number): + self.add_error('social_security_number', "Social security number is invalid") + + +def is_valid_social_security_number(number): + if number is not None and number != '': + if len(number) != 13: + return False + if not number.isdigit(): + return False + if not LuhnAlgorithm.is_luhn_valid(number[:12]): + return False + if not VerhoeffAlgorithm.is_valid_verhoeff(number[:11] + number[12]): + return False + + return True + + 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"] @@ -32,6 +56,7 @@ class SubjectAddForm(ModelForm): def clean(self): cleaned_data = super(SubjectAddForm, self).clean() validate_subject_country(self, cleaned_data) + validate_social_security_number(self, cleaned_data["social_security_number"]) return cleaned_data @@ -53,6 +78,7 @@ class SubjectEditForm(ModelForm): def clean(self): validate_subject_country(self, self.cleaned_data) + validate_social_security_number(self, self.cleaned_data["social_security_number"]) class Meta: model = Subject diff --git a/smash/web/forms/voucher_forms.py b/smash/web/forms/voucher_forms.py new file mode 100644 index 0000000000000000000000000000000000000000..99bb89736762a3c49a1e859d95feb0389f033fa9 --- /dev/null +++ b/smash/web/forms/voucher_forms.py @@ -0,0 +1,81 @@ +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): + voucher_types = kwargs.pop('voucher_types', VoucherType.objects.all()) + super(VoucherForm, self).__init__(*args, **kwargs) + + self.fields['voucher_type'].queryset = voucher_types + + 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/migrations/0097_auto_20171211_1616.py b/smash/web/migrations/0097_auto_20171211_1616.py new file mode 100644 index 0000000000000000000000000000000000000000..676c94f20f7f1553d4cc15445331f6b18548b849 --- /dev/null +++ b/smash/web/migrations/0097_auto_20171211_1616.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-11 16:16 +from __future__ import unicode_literals + +import django.core.files.storage +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0096_auto_20171208_1509'), + ] + + operations = [ + migrations.AddField( + model_name='studycolumns', + name='health_partner', + field=models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=False, verbose_name=b'Health partner'), + ), + migrations.AddField( + model_name='studycolumns', + name='health_partner_feedback_agreement', + field=models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=False, verbose_name=b'Agrees to give information to referral'), + ), + migrations.AddField( + model_name='studycolumns', + name='previously_in_study', + field=models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=False, verbose_name=b'Previously in PDP study'), + ), + migrations.AddField( + model_name='studycolumns', + name='referral_letter', + field=models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=False, verbose_name=b'Referral letter'), + ), + migrations.AddField( + model_name='studycolumns', + name='screening', + field=models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=False, verbose_name=b'Screening'), + ), + migrations.AddField( + model_name='studycolumns', + name='voucher_types', + field=models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=False, verbose_name=b'Voucher types'), + ), + migrations.AddField( + model_name='studycolumns', + name='vouchers', + field=models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=False, verbose_name=b'Vouchers'), + ), + migrations.AddField( + model_name='studysubject', + name='health_partner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='web.Worker', verbose_name=b'Health partner'), + ), + migrations.AddField( + model_name='studysubject', + name='health_partner_feedback_agreement', + field=models.BooleanField(default=False, verbose_name=b'Agrees to give information to referral'), + ), + migrations.AddField( + model_name='studysubject', + name='previously_in_study', + field=models.BooleanField(default=False, verbose_name=b'Previously in PDP study'), + ), + migrations.AddField( + model_name='studysubject', + name='referral_letter', + field=models.FileField(blank=True, null=True, storage=django.core.files.storage.FileSystemStorage(location=b'uploads'), upload_to=b'referral_letters', verbose_name=b'Referral letter'), + ), + migrations.AddField( + model_name='studysubject', + name='screening', + field=models.CharField(blank=True, max_length=1024, null=True, verbose_name=b'Screening'), + ), + migrations.AddField( + model_name='studysubject', + name='voucher_types', + field=models.ManyToManyField(blank=True, to='web.VoucherType', verbose_name=b'Voucher types'), + ), + migrations.AlterField( + model_name='studycolumns', + name='datetime_contact_reminder', + field=models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=True, verbose_name=b'Please make a contact on'), + ), + migrations.AlterField( + model_name='studysubject', + name='diagnosis', + field=models.CharField(blank=True, max_length=1024, null=True, verbose_name=b'Diagnosis'), + ), + migrations.AlterField( + model_name='voucher', + name='study_subject', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='vouchers', to='web.StudySubject'), + ), + ] 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..5db58bb977dc42c08b437e45c91ca1e542e8be0b 100644 --- a/smash/web/models/constants.py +++ b/smash/web/models/constants.py @@ -1,6 +1,8 @@ # coding=utf-8 import locale +from django.core.files.storage import FileSystemStorage + BOOL_CHOICES = ((True, 'Yes'), (False, 'No')) SEX_CHOICES_MALE = 'M' SEX_CHOICES_FEMALE = 'F' @@ -81,3 +83,14 @@ 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'), +) + +FILE_STORAGE = FileSystemStorage(location='uploads') diff --git a/smash/web/models/study_columns.py b/smash/web/models/study_columns.py index 7700a61be37532167f324d701fd8c771d9aecbe1..7acd12ba62fbfe7c027c02d5943bd28723b80c2d 100644 --- a/smash/web/models/study_columns.py +++ b/smash/web/models/study_columns.py @@ -78,7 +78,37 @@ class StudyColumns(models.Model): verbose_name='Resign reason' ) - datetime_contact_reminder = models.BooleanField(choices=BOOL_CHOICES, - default=True, - verbose_name='Last contact attempt' - ) + referral_letter = models.BooleanField(choices=BOOL_CHOICES, + default=False, + verbose_name='Referral letter' + ) + + health_partner = models.BooleanField(choices=BOOL_CHOICES, + default=False, + verbose_name='Health partner' + ) + + health_partner_feedback_agreement = models.BooleanField(choices=BOOL_CHOICES, + default=False, + verbose_name='Agrees to give information to referral' + ) + + screening = models.BooleanField(choices=BOOL_CHOICES, + default=False, + verbose_name='Screening' + ) + + previously_in_study = models.BooleanField(choices=BOOL_CHOICES, + default=False, + verbose_name='Previously in PDP study', + ) + + voucher_types = models.BooleanField(choices=BOOL_CHOICES, + default=False, + verbose_name='Voucher types', + ) + + vouchers = models.BooleanField(choices=BOOL_CHOICES, + default=False, + verbose_name='Vouchers', + ) diff --git a/smash/web/models/study_subject.py b/smash/web/models/study_subject.py index 8bbeae76e223005167fc857d97465eff1c92af4f..705bd1905411777e0de4a7b57a52960f13278e99 100644 --- a/smash/web/models/study_subject.py +++ b/smash/web/models/study_subject.py @@ -1,9 +1,8 @@ # coding=utf-8 -from django.core.validators import RegexValidator from django.db import models -from constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES -from . import Appointment, Location, Visit +from web.models import VoucherType, Appointment, Location, Visit +from web.models.constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES, FILE_STORAGE class StudySubject(models.Model): @@ -92,11 +91,47 @@ class StudySubject(models.Model): blank=True, verbose_name='Referred by' ) - diagnosis = models.CharField(max_length=128, + referral_letter = models.FileField( + storage=FILE_STORAGE, + upload_to='referral_letters', + verbose_name='Referral letter', + blank=True, + null=True, + ) + + health_partner = models.ForeignKey("web.Worker", + verbose_name='Health partner', + null=True, + blank=True + ) + + health_partner_feedback_agreement = models.BooleanField( + verbose_name='Agrees to give information to referral', + default=False, + ) + + screening = models.CharField(max_length=1024, + null=True, + blank=True, + verbose_name='Screening' + ) + + diagnosis = models.CharField(max_length=1024, null=True, blank=True, verbose_name='Diagnosis' ) + + previously_in_study = models.BooleanField( + verbose_name='Previously in PDP study', + default=False, + ) + + voucher_types = models.ManyToManyField(VoucherType, + blank=True, + verbose_name='Voucher types' + ) + year_of_diagnosis = models.IntegerField( null=True, blank=True, diff --git a/smash/web/models/voucher.py b/smash/web/models/voucher.py new file mode 100644 index 0000000000000000000000000000000000000000..340b78764e16e537b72809a29f767811b105650d --- /dev/null +++ b/smash/web/models/voucher.py @@ -0,0 +1,59 @@ +# 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, + related_name="vouchers", + 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/appointments/edit.html b/smash/web/templates/appointments/edit.html index 1d7cfa2ab4d86d3105f38041af240c4202339785..dc1df827cfd7967dbad32f7e7e69ae79545bacd9 100644 --- a/smash/web/templates/appointments/edit.html +++ b/smash/web/templates/appointments/edit.html @@ -35,7 +35,7 @@ </div> <div class="box box-info"> - <form method="post" action="" class="form-horizontal"> + <form method="post" action="" enctype="multipart/form-data" class="form-horizontal"> {% csrf_token %} <fieldset> <div class="box-header with-border"> @@ -136,6 +136,11 @@ </div><!-- /.box-footer --> </form> </div> + + {% if appointment.visit.subject.study.columns.vouchers %} + {% include 'includes/subject_vouchers_box.html' with subject=appointment.visit.subject %} + {% endif %} + {% include 'includes/mail_templates_box.html' with instance_id=appointment.id %} {% if appointment.visit %} {% include 'includes/contact_attempts_box.html' with subject=appointment.visit.subject contact_attempts=contact_attempts appointment_id=appointment.id %} diff --git a/smash/web/templates/includes/subject_vouchers_box.html b/smash/web/templates/includes/subject_vouchers_box.html new file mode 100644 index 0000000000000000000000000000000000000000..36638b10787183d64546baaa7fc5d2ec3810f496 --- /dev/null +++ b/smash/web/templates/includes/subject_vouchers_box.html @@ -0,0 +1,48 @@ +<div class="row"> + <div class="col-lg-12"> + <div class="box box-success"> + <div class="box-header with-border"> + <h3>Vouchers <a title="add a new voucher" + id="add-voucher" + href="{% url 'web.views.voucher_add' %}?study_subject_id={{ subject.id }}" + class="text-primary" + ><i class="fa fa-plus-circle text-success"></i></a></h3> + </div> + <div class="box-body"> + <table class="table table-bordered table-striped"> + <thead> + <tr> + + <th class="text-center">Number</th> + <th class="text-center">Type</th> + <th class="text-center">Issue date</th> + <th class="text-center">Expiry date</th> + <th class="text-center">Status</th> + <th class="text-center">Use date</th> + <th class="text-center">Partner</th> + <th class="text-center">Feedback</th> + <th class="text-center">Edit</th> + </tr> + </thead> + <tbody> + {% for voucher in subject.vouchers.all %} + <tr> + <td>{{ voucher.number }}</td> + <td>{{ voucher.voucher_type }}</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> + </div> + + </div> +</div> diff --git a/smash/web/templates/sidebar.html b/smash/web/templates/sidebar.html index 70f1ff705b17df0f826c47b921d33f2ed6741c24..05498fc5bf407bdd307a8e554214a598a5105953 100644 --- a/smash/web/templates/sidebar.html +++ b/smash/web/templates/sidebar.html @@ -63,6 +63,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/subjects/add.html b/smash/web/templates/subjects/add.html index 3bacf8021599c202596190da36dc00db059e1f88..bf01448f4625f7028478f21787460fabb27497d8 100644 --- a/smash/web/templates/subjects/add.html +++ b/smash/web/templates/subjects/add.html @@ -31,7 +31,7 @@ </div> <div class="row"> - <form method="post" action="" class="form-horizontal"> + <form method="post" action="" enctype="multipart/form-data" class="form-horizontal"> {% csrf_token %} <div class="col-md-12"> <div class="box box-body"> diff --git a/smash/web/templates/subjects/edit.html b/smash/web/templates/subjects/edit.html index 7e956ad711830c9c1e318edb5b474346910ff521..669f863086174f60aaca05907bf6713c84725340 100644 --- a/smash/web/templates/subjects/edit.html +++ b/smash/web/templates/subjects/edit.html @@ -41,7 +41,7 @@ <h3>Subject details</h3> </div> - <form method="post" action="" class="form-horizontal"> + <form method="post" action="" enctype="multipart/form-data" class="form-horizontal"> {% csrf_token %} <div class="box-body"> <div class="col-md-12"> @@ -109,11 +109,16 @@ </div><!-- /.col-md-12 --> </div><!-- /.row --> + {% if study_subject.study.columns.vouchers %} + {% include 'includes/subject_vouchers_box.html' with subject=study_subject %} + {% endif %} + {% include 'includes/mail_templates_box.html' with instance_id=study_subject.id %} {% include 'includes/contact_attempts_box.html' with subject=study_subject contact_attempts=contact_attempts %} + <div class="modal modal-danger fade" id="confirm-dead-resigned-mark-dialog" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> 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/api_views/test_subject.py b/smash/web/tests/api_views/test_subject.py index 2ed3fc0a5721f39de92e27066bbc28ce6c362882..3b4ad50d6f1af4e47c121b1ce4e78f9e7c1a487b 100644 --- a/smash/web/tests/api_views/test_subject.py +++ b/smash/web/tests/api_views/test_subject.py @@ -5,14 +5,14 @@ import logging from django.urls import reverse -from web.tests import LoggedInWithWorkerTestCase from web.api_views.subject import get_subjects_order, get_subjects_filtered, serialize_subject -from web.models import StudySubject, Appointment, Study +from web.models import StudySubject, Appointment, Study, Worker from web.models.constants import GLOBAL_STUDY_ID, SUBJECT_TYPE_CHOICES_PATIENT, SUBJECT_TYPE_CHOICES_CONTROL from web.models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \ StudySubjectList +from web.tests import LoggedInWithWorkerTestCase from web.tests.functions import create_study_subject, create_get_suffix, create_visit, \ - create_appointment, create_empty_study_columns, create_contact_attempt, create_flying_team + create_appointment, create_empty_study_columns, create_contact_attempt, create_flying_team, create_worker from web.views.notifications import get_today_midnight_date logger = logging.getLogger(__name__) @@ -384,13 +384,14 @@ class TestSubjectApi(LoggedInWithWorkerTestCase): self.check_subject_filtered([["some_unknown", "unknown data"]], [subject]) self.check_subject_filtered([["", ""]], [subject]) - self.check_subject_filtered([["", None]], [subject]) + self.check_subject_filtered([[None, None]], [subject]) def test_serialize_subject(self): study_subject = self.study_subject study_subject.subject.dead = True study_subject.flying_team = create_flying_team() study_subject.datetime_contact_reminder = get_today_midnight_date() + study_subject.health_partner = create_worker() create_contact_attempt(subject=study_subject) create_visit(subject=study_subject) @@ -534,3 +535,68 @@ class TestSubjectApi(LoggedInWithWorkerTestCase): create_contact_attempt(subject=subject) self.check_subject_ordered("last_contact_attempt", [subject, subject2]) + + def test_subjects_ordered_by_information_sent(self): + subject = self.study_subject + subject.information_sent = False + subject.save() + subject2 = create_study_subject(2) + subject2.information_sent = True + subject2.save() + + self.check_subject_ordered("information_sent", [subject, subject2]) + + def test_subjects_ordered_by_type(self): + subject = self.study_subject + subject.type = SUBJECT_TYPE_CHOICES_CONTROL + subject.save() + subject2 = create_study_subject(2) + subject2.type = SUBJECT_TYPE_CHOICES_PATIENT + subject2.save() + + self.check_subject_ordered("type", [subject, subject2]) + + def test_subjects_ordered_by_social_security_number(self): + subject = self.study_subject + subject.subject.social_security_number = "01" + subject.subject.save() + subject2 = create_study_subject(2) + subject2.subject.social_security_number = "02" + subject2.subject.save() + + self.check_subject_ordered("social_security_number", [subject, subject2]) + + def test_subjects_ordered_by_health_partner(self): + subject = self.study_subject + subject.health_partner = Worker.objects.create(first_name='first1', last_name="name2222", email='jacob@bla', ) + subject.save() + subject2 = create_study_subject(2) + subject2.health_partner = Worker.objects.create(first_name='first2', last_name="name1111", email='jacob@bla', ) + subject2.save() + + self.check_subject_ordered("health_partner_first_name", [subject, subject2]) + self.check_subject_ordered("health_partner_last_name", [subject2, subject]) + + def test_subjects_filter_health_partner_first_name(self): + subject = self.study_subject + subject.health_partner = Worker.objects.create(first_name='first1', last_name="name2222", email='jacob@bla', ) + subject.save() + + self.check_subject_filtered([["health_partner_first_name", "first1"]], [subject]) + self.check_subject_filtered([["health_partner_first_name", "unknown"]], []) + + def test_subjects_filter_health_partner_last_name(self): + subject = self.study_subject + subject.health_partner = Worker.objects.create(first_name='first1', last_name="name2222", email='jacob@bla', ) + subject.save() + + self.check_subject_filtered([["health_partner_last_name", "name2222"]], [subject]) + self.check_subject_filtered([["health_partner_last_name", "unknown"]], []) + + def test_subjects_filter_social_security_number(self): + subject = self.study_subject.subject + subject.social_security_number = "123" + subject.save() + + self.check_subject_filtered([["social_security_number", "12"]], [self.study_subject]) + self.check_subject_filtered([["social_security_number", "unknown"]], []) diff --git a/smash/web/tests/forms/test_StudySubjectEditForm.py b/smash/web/tests/forms/test_StudySubjectEditForm.py index 1e7a5c0919c5b3ed775b23e1cf4bb1b3ab538f59..da51f0a6a8d77fbdf15b5b35a6805ee1a6fbfa77 100644 --- a/smash/web/tests/forms/test_StudySubjectEditForm.py +++ b/smash/web/tests/forms/test_StudySubjectEditForm.py @@ -29,6 +29,7 @@ class StudySubjectEditFormTests(LoggedInWithWorkerTestCase): def test_validation(self): edit_form = StudySubjectEditForm(self.sample_data) save_status = edit_form.is_valid() + logger.debug(edit_form.errors) self.assertTrue(save_status) def test_validation_with_empty_study(self): diff --git a/smash/web/tests/forms/test_subject_forms.py b/smash/web/tests/forms/test_subject_forms.py new file mode 100644 index 0000000000000000000000000000000000000000..fe109455c9595a3b3ee475a27ff23509e41a2e1b --- /dev/null +++ b/smash/web/tests/forms/test_subject_forms.py @@ -0,0 +1,29 @@ +import logging + +from web.forms.subject_forms import is_valid_social_security_number +from web.tests import LoggedInWithWorkerTestCase +from web.tests.functions import create_subject + +logger = logging.getLogger(__name__) + + +class StudySubjectAddFormTests(LoggedInWithWorkerTestCase): + def setUp(self): + super(StudySubjectAddFormTests, self).setUp() + self.subject = create_subject() + + def test_is_valid_social_security_number_too_short(self): + self.assertFalse(is_valid_social_security_number("123")) + + def test_is_valid_social_security_number_not_a_number(self): + # noinspection SpellCheckingInspection + self.assertFalse(is_valid_social_security_number("ABCDEFGHIJKLM")) + + def test_is_valid_social_security_number_invalid(self): + self.assertFalse(is_valid_social_security_number("1234567890123")) + + def test_is_valid_social_security_number(self): + self.assertTrue(is_valid_social_security_number("1893120105732")) + + def test_is_valid_social_security_number_empty(self): + self.assertTrue(is_valid_social_security_number("")) 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..64a78eef15b2365f515aecfa030b4da88aaeb166 --- /dev/null +++ b/smash/web/tests/forms/test_voucher_forms.py @@ -0,0 +1,73 @@ +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): + voucher_type = create_voucher_type() + study_subject = create_study_subject() + study_subject.voucher_types.add(voucher_type) + create_voucher(study_subject) + + voucher_form = VoucherForm() + form_data = { + "status": VOUCHER_STATUS_USED, + "usage_partner": str(self.worker.id), + "voucher_type": 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..8261bd6335145cc090f610467b3a7c6523eaeaef 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 @@ -249,6 +260,8 @@ def format_form_field(value): return value.strftime('%Y-%m-%d') elif isinstance(value, datetime.datetime): return value.strftime('%Y-%m-%d %H:%M') + elif value is None: + return "" else: return value 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_appointments.py b/smash/web/tests/view/test_appointments.py index add77509ac5daeefdd5e8e34d23311f7af083c0f..9560cfa02a577b900b541addbf304c98e0506747 100644 --- a/smash/web/tests/view/test_appointments.py +++ b/smash/web/tests/view/test_appointments.py @@ -1,6 +1,7 @@ import datetime import logging +from django.core.files.uploadedfile import SimpleUploadedFile from django.urls import reverse from web.forms import AppointmentEditForm, SubjectEditForm, StudySubjectEditForm @@ -171,17 +172,15 @@ class AppointmentsViewTests(LoggedInTestCase): def prepare_form(self, appointment, subject): form_appointment = AppointmentEditForm(user=self.user, instance=appointment, prefix="appointment") form_study_subject = StudySubjectEditForm(instance=subject, prefix="study-subject") - form_subject = SubjectEditForm(instance=subject.subject, prefix="study-subject") + form_subject = SubjectEditForm(instance=subject.subject, prefix="subject") form_data = {} for key, value in form_appointment.initial.items(): - if value is not None: - form_data['appointment-{}'.format(key)] = format_form_field(value) + form_data['appointment-{}'.format(key)] = format_form_field(value) for key, value in form_study_subject.initial.items(): - if value is not None: - form_data['study-subject-{}'.format(key)] = format_form_field(value) + form_data['study-subject-{}'.format(key)] = format_form_field(value) for key, value in form_subject.initial.items(): - if value is not None: - form_data['subject-{}'.format(key)] = format_form_field(value) + form_data['subject-{}'.format(key)] = format_form_field(value) + form_data["study-subject-referral_letter"] = SimpleUploadedFile("file.txt", b"file_content") return form_data def test_subject_flying_team_location(self): 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_subjects.py b/smash/web/tests/view/test_subjects.py index cf9d49777f4ebd58988db0c48c3e8df2d6eb509e..c84a8d6e126f08d90009851e721a4486d3e5cd4c 100644 --- a/smash/web/tests/view/test_subjects.py +++ b/smash/web/tests/view/test_subjects.py @@ -1,15 +1,16 @@ import datetime import logging +from django.core.files.uploadedfile import SimpleUploadedFile from django.urls import reverse from web.forms import SubjectAddForm, SubjectEditForm, StudySubjectAddForm, StudySubjectEditForm -from web.models import MailTemplate, StudySubject +from web.models import MailTemplate, StudySubject, StudyColumns from web.models.constants import SEX_CHOICES_MALE, SUBJECT_TYPE_CHOICES_CONTROL, SUBJECT_TYPE_CHOICES_PATIENT, \ COUNTRY_AFGHANISTAN_ID, COUNTRY_OTHER_ID, MAIL_TEMPLATE_CONTEXT_SUBJECT from web.tests import LoggedInWithWorkerTestCase from web.tests.functions import create_study_subject, create_visit, create_appointment, get_test_location, \ - create_language, get_resource_path, get_test_study + create_language, get_resource_path, get_test_study, format_form_field from web.views.notifications import get_today_midnight_date logger = logging.getLogger(__name__) @@ -75,8 +76,9 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase): form_data['subject-dead'] = "True" form_data['study_subject-resigned'] = "True" form_data['study_subject-resign_reason'] = "doesn't want to participate" - response = self.client.post( - reverse('web.views.subject_edit', kwargs={'id': self.study_subject.id}), data=form_data) + + url = reverse('web.views.subject_edit', kwargs={'id': self.study_subject.id}) + response = self.client.post(url, data=form_data) self.assertEqual(response.status_code, 302) updated_study_subject = StudySubject.objects.filter(id=self.study_subject.id)[0] @@ -98,11 +100,10 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase): form_subject = SubjectEditForm(instance=self.study_subject.subject, prefix="subject") form_data = {} for key, value in form_study_subject.initial.items(): - if value is not None: - form_data['study_subject-{}'.format(key)] = value + form_data['study_subject-{}'.format(key)] = format_form_field(value) for key, value in form_subject.initial.items(): - if value is not None: - form_data['subject-{}'.format(key)] = value + form_data['subject-{}'.format(key)] = format_form_field(value) + form_data["study_subject-referral_letter"] = SimpleUploadedFile("file.txt", b"file_content") return form_data def create_add_form_data_for_study_subject(self): @@ -110,11 +111,9 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase): form_subject = SubjectAddForm(prefix="subject") form_data = {} for key, value in form_study_subject.initial.items(): - if value is not None: - form_data['study_subject-{}'.format(key)] = value + form_data['study_subject-{}'.format(key)] = format_form_field(value) for key, value in form_subject.initial.items(): - if value is not None: - form_data['subject-{}'.format(key)] = value + form_data['subject-{}'.format(key)] = format_form_field(value) self.add_valid_form_data_for_subject_add(form_data) return form_data @@ -133,6 +132,30 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase): "prefix should start by L" + " as default location prefix is not defined and subject type is control") + def test_subjects_add_with_referral_letter_file(self): + StudyColumns.objects.all().update(referral_letter=True) + + form_data = self.create_add_form_data_for_study_subject() + + form_data["study_subject-type"] = SUBJECT_TYPE_CHOICES_CONTROL + form_data["study_subject-default_location"] = get_test_location().id + form_data["study_subject-referral_letter"] = SimpleUploadedFile("file.txt", b"file_content") + response = self.client.post(reverse('web.views.subject_add'), data=form_data) + self.assertEqual(response.status_code, 302) + response = self.client.get(response.url) + self.assertContains(response, "Subject created") + + subject = StudySubject.objects.all().order_by("-id")[0] + + # check if edit page renders properly + response = self.client.get(reverse('web.views.subject_edit', kwargs={'id': subject.id})) + self.assertEqual(response.status_code, 200) + + # check if file can be downloaded + url = reverse('web.views.uploaded_files') + "?file=" + unicode(subject.referral_letter) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + def add_valid_form_data_for_subject_add(self, form_data): form_data["subject-country"] = COUNTRY_AFGHANISTAN_ID form_data["subject-first_name"] = "John" diff --git a/smash/web/tests/view/test_voucher.py b/smash/web/tests/view/test_voucher.py new file mode 100644 index 0000000000000000000000000000000000000000..583328498a851e367e7ca832f9b581cdc2316bcc --- /dev/null +++ b/smash/web/tests/view/test_voucher.py @@ -0,0 +1,58 @@ +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): + study_subject = create_study_subject() + url = reverse('web.views.voucher_add') + "?study_subject_id=" + str(study_subject.id) + response = self.client.get(url) + 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): + voucher_type = create_voucher_type() + study_subject = create_study_subject() + study_subject.voucher_types.add(voucher_type) + visit_detail_form = VoucherForm() + form_data = { + "status": VOUCHER_STATUS_NEW, + "voucher_type": 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 7b1137c65d321436acb0ae4f3bc84cef7be342e3..c58aa7bb6a3cebcf9310718f5c7042e1ca58e02f 100644 --- a/smash/web/urls.py +++ b/smash/web/urls.py @@ -181,6 +181,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 # #################### @@ -201,6 +209,12 @@ urlpatterns = [ url(r'^configuration$', views.configuration_item.configuration_items, name='web.views.configuration'), + #################### + # FILES # + #################### + + url(r'^files/', views.uploaded_files.download, name='web.views.uploaded_files'), + #################### # AUTH # #################### diff --git a/smash/web/views/__init__.py b/smash/web/views/__init__.py index 461b8b16f33c2172552b8b5a59656ef29dff30db..fedbe5fd8b6300d36166e6e9ba058174553a0352 100644 --- a/smash/web/views/__init__.py +++ b/smash/web/views/__init__.py @@ -2,6 +2,7 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.shortcuts import redirect, render +from django.utils.decorators import method_decorator from django.views.generic.base import ContextMixin from notifications import get_notifications @@ -53,11 +54,15 @@ def extend_context(params, request): return final_params +@method_decorator(login_required, name='dispatch') class WrappedView(ContextMixin): def get_context_data(self, **kwargs): context = super(WrappedView, self).get_context_data(**kwargs) return extend_context(context, self.request) + def dispatch(self, *args, **kwargs): + return super(WrappedView, self).dispatch(*args, **kwargs) + import auth import appointment @@ -73,6 +78,8 @@ import export import contact_attempt import configuration_item import language +import voucher import voucher_type import voucher_type_price import redcap +import uploaded_files diff --git a/smash/web/views/appointment.py b/smash/web/views/appointment.py index 6d64acd468487ffc1a569241661c309d5b12f5ae..fac01b5a1d1aabe6537fb641e13612734b3a6e76 100644 --- a/smash/web/views/appointment.py +++ b/smash/web/views/appointment.py @@ -139,7 +139,6 @@ def appointment_edit(request, id): 'appointment_form': appointment_form, 'study_subject_form': study_subject_form, 'subject_form': subject_form, - 'id': id, 'appointment': the_appointment, 'contact_attempts': contact_attempts, 'mail_templates': MailTemplate.get_appointment_mail_templates(languages) diff --git a/smash/web/views/subject.py b/smash/web/views/subject.py index 5825db2be3767e1075badcd3096e6580cfa20cec..f1c9566a8be16acd252dc146bfcf3c06be5165b9 100644 --- a/smash/web/views/subject.py +++ b/smash/web/views/subject.py @@ -2,6 +2,7 @@ import logging from django.contrib import messages +from django.contrib.auth.decorators import login_required from django.shortcuts import redirect, get_object_or_404 from ..models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT @@ -21,6 +22,7 @@ def subjects(request): return wrap_response(request, 'subjects/index.html', context) +@login_required def subject_add(request): study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] if request.method == 'POST': diff --git a/smash/web/views/uploaded_files.py b/smash/web/views/uploaded_files.py new file mode 100644 index 0000000000000000000000000000000000000000..e20a022a77488aabc8e7365352ff8f3b60990a54 --- /dev/null +++ b/smash/web/views/uploaded_files.py @@ -0,0 +1,25 @@ +# coding=utf-8 +import logging +import ntpath +from wsgiref.util import FileWrapper + +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse + +from web.models.constants import FILE_STORAGE + +logger = logging.getLogger(__name__) + + +def path_to_filename(path): + head, tail = ntpath.split(path) + return tail or ntpath.basename(head) + + +@login_required +def download(request): + if request.GET and request.GET.get('file'): + path = FILE_STORAGE.location + "/" + request.GET.get('file') + response = HttpResponse(FileWrapper(open(path, 'r')), content_type='application/force-download') + response['Content-Disposition'] = 'attachment; filename=%s' % path_to_filename(path) + return response diff --git a/smash/web/views/voucher.py b/smash/web/views/voucher.py new file mode 100644 index 0000000000000000000000000000000000000000..4ca8361cbfb391c347a85db4a4ad38e1bef77266 --- /dev/null +++ b/smash/web/views/voucher.py @@ -0,0 +1,66 @@ +# 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, StudySubject +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' + + +def voucher_types_for_study_subject(study_subject_id): + return StudySubject.objects.get(id=study_subject_id).voucher_types.all() + + +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)}) + + def get_form_kwargs(self): + kwargs = super(VoucherCreateView, self).get_form_kwargs() + kwargs['voucher_types'] = voucher_types_for_study_subject(self.request.GET.get("study_subject_id", -1)) + return kwargs + + +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 + return reverse_lazy('web.views.subject_edit', kwargs={'id': self.get_study_subject_id()}) + + def get_study_subject_id(self): + return Voucher.objects.get(id=self.kwargs['pk']).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 diff --git a/smash/web/widgets/__init__.py b/smash/web/widgets/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/smash/web/widgets/secure_file_widget.py b/smash/web/widgets/secure_file_widget.py new file mode 100644 index 0000000000000000000000000000000000000000..d26d799f1ea97c6414b865633183427ec973bba0 --- /dev/null +++ b/smash/web/widgets/secure_file_widget.py @@ -0,0 +1,26 @@ +import logging + +from django import forms +from django.core.urlresolvers import reverse +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ + +logger = logging.getLogger(__name__) + + +class SecuredFileWidget(forms.FileInput): + """A FileField Widget that shows secure file link""" + + def __init__(self, attrs=None): + if attrs is None: + attrs = {} + super(SecuredFileWidget, self).__init__(attrs) + + def render(self, name, value, attrs=None): + output = [] + if value and hasattr(value, "url"): + url = reverse('web.views.uploaded_files') + '?file=' + unicode(value) + out = u'<a href="{}">{}</a><br />{} ' + output.append(out.format(url, _(u'Download'), _(u'Change:'))) + output.append(super(SecuredFileWidget, self).render(name, value, attrs)) + return mark_safe(u''.join(output))