diff --git a/smash/smash/settings.py b/smash/smash/settings.py index c11fe16accedc21a7dcdf46f168e4305be7ec5ea..14f7a7ff96dc662eadb5b2216029619d0f16cc28 100644 --- a/smash/smash/settings.py +++ b/smash/smash/settings.py @@ -76,6 +76,7 @@ CRON_CLASSES = [ 'web.views.kit.KitRequestEmailSendJob', 'web.redcap_connector.RedCapRefreshJob', 'web.views.voucher.ExpireVouchersJob', + 'web.importer.importer_cron_job.ImporterCronJob' ] # Password validation diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py index fb81d9624b40750c5f24315112d873516b2ff3ab..c31d13903ab43b26fa7db0fd1f9c570d28e5cb5e 100644 --- a/smash/web/api_views/subject.py +++ b/smash/web/api_views/subject.py @@ -70,6 +70,10 @@ def get_subject_columns(request, subject_list_type): add_column(result, "Resigned", "resigned", study_subject_columns, "yes_no_filter", study.columns) add_column(result, "Endpoint Reached", "endpoint_reached", study_subject_columns, "yes_no_filter", study.columns) add_column(result, "Postponed", "postponed", study_subject_columns, "yes_no_filter", study.columns) + add_column(result, "Next of keen", "next_of_keen_name", subject_columns, "string_filter") + add_column(result, "Next of keen phone", "next_of_keen_phone", subject_columns, "string_filter") + add_column(result, "Next of keen address", "next_of_keen_address", subject_columns, "string_filter") + add_column(result, "Brain donation agreement", "brain_donation_agreement", study_subject_columns, "yes_no_filter", study.columns) add_column(result, "Excluded", "excluded", study_subject_columns, "yes_no_filter", study.columns) add_column(result, "Info sent", "information_sent", study_subject_columns, "yes_no_filter", study.columns) add_column(result, "Type", "type", study_subject_columns, "type_filter", study.columns) @@ -113,6 +117,12 @@ def get_subjects_order(subjects_to_be_ordered, order_column, order_direction, co result = subjects_to_be_ordered.order_by(order_direction + 'subject__last_name') elif order_column == "address": result = subjects_to_be_ordered.order_by(order_direction + 'subject__address') + elif order_column == "next_of_keen_name": + result = subjects_to_be_ordered.order_by(order_direction + 'subject__next_of_keen_name') + elif order_column == "next_of_keen_phone": + result = subjects_to_be_ordered.order_by(order_direction + 'subject__next_of_keen_phone') + elif order_column == "next_of_keen_address": + result = subjects_to_be_ordered.order_by(order_direction + 'subject__next_of_keen_address') elif order_column == "nd_number": result = subjects_to_be_ordered.order_by(order_direction + 'nd_number') elif order_column == "referral": @@ -144,6 +154,8 @@ def get_subjects_order(subjects_to_be_ordered, order_column, order_direction, co 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 == "brain_donation_agreement": + result = subjects_to_be_ordered.order_by(order_direction + 'brain_donation_agreement') elif order_column == "excluded": result = subjects_to_be_ordered.order_by(order_direction + 'excluded') elif order_column == "type": @@ -242,6 +254,12 @@ def get_subjects_filtered(subjects_to_be_filtered, filters): result = result.filter(subject__last_name__icontains=value) elif column == "address": result = result.filter(subject__address__icontains=value) + elif column == "next_of_keen_name": + result = result.filter(subject__next_of_keen_name__icontains=value) + elif column == "next_of_keen_phone": + result = result.filter(subject__next_of_keen_phone__icontains=value) + elif column == "next_of_keen_address": + result = result.filter(subject__next_of_keen_address__icontains=value) elif column == "nd_number": result = result.filter(nd_number__icontains=value) elif column == "referral": @@ -254,6 +272,8 @@ def get_subjects_filtered(subjects_to_be_filtered, filters): result = result.filter(resigned=(value == "true")) elif column == "endpoint_reached": result = result.filter(endpoint_reached=(value == "true")) + elif column == "brain_donation_agreement": + result = result.filter(brain_donation_agreement=(value == "true")) elif column == "postponed": result = result.filter(postponed=(value == "true")) elif column == "excluded": @@ -402,6 +422,9 @@ def serialize_subject(study_subject): "first_name": study_subject.subject.first_name, "last_name": study_subject.subject.last_name, "address": study_subject.subject.pretty_address(), + "next_of_keen_name": study_subject.subject.next_of_keen_name, + "next_of_keen_phone": study_subject.subject.next_of_keen_phone, + "next_of_keen_address": study_subject.subject.next_of_keen_address, "date_born": study_subject.subject.date_born, "datetime_contact_reminder": contact_reminder, "last_contact_attempt": last_contact_attempt_string, @@ -414,6 +437,7 @@ def serialize_subject(study_subject): "resigned": bool_to_yes_no(study_subject.resigned), "endpoint_reached": bool_to_yes_no(study_subject.endpoint_reached), "postponed": bool_to_yes_no(study_subject.postponed), + "brain_donation_agreement": bool_to_yes_no(study_subject.brain_donation_agreement), "excluded": bool_to_yes_no(study_subject.excluded), "information_sent": bool_to_yes_no(study_subject.information_sent), "health_partner_first_name": health_partner_first_name, diff --git a/smash/web/forms/study_subject_forms.py b/smash/web/forms/study_subject_forms.py index ea2849fc6ae8ddce7b8f5ddc62d1bb7467908b8a..2bc880780226734cfa1653501960eb47bc8138b0 100644 --- a/smash/web/forms/study_subject_forms.py +++ b/smash/web/forms/study_subject_forms.py @@ -161,6 +161,7 @@ def prepare_study_subject_fields(fields, study): prepare_field(fields, study.columns, 'nd_number') prepare_field(fields, study.columns, 'datetime_contact_reminder') prepare_field(fields, study.columns, 'postponed') + prepare_field(fields, study.columns, 'brain_donation_agreement') prepare_field(fields, study.columns, 'flying_team') prepare_field(fields, study.columns, 'mpower_id') prepare_field(fields, study.columns, 'comments') diff --git a/smash/web/importer/__init__.py b/smash/web/importer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..eda42f3accab2707cf3151273f71db973c51f267 --- /dev/null +++ b/smash/web/importer/__init__.py @@ -0,0 +1,8 @@ +from csv_subject_import_reader import CsvSubjectImportReader +from exporter import Exporter +from exporter_cron_job import ExporterCronJob +from importer import Importer +from importer_cron_job import ImporterCronJob +from subject_import_reader import SubjectImportReader + +__all__ = [Importer, SubjectImportReader, CsvSubjectImportReader, ImporterCronJob, Exporter, ExporterCronJob] diff --git a/smash/web/importer/csv_subject_import_reader.py b/smash/web/importer/csv_subject_import_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..187b59aba9d98798f8630054828256aa3b80b07f --- /dev/null +++ b/smash/web/importer/csv_subject_import_reader.py @@ -0,0 +1,74 @@ +import csv +import datetime +import logging + +from subject_import_reader import SubjectImportReader +from web.models import StudySubject, Subject, Study +from web.models.constants import GLOBAL_STUDY_ID + +CSV_DATE_FORMAT = "%d-%m-%Y" + +logger = logging.getLogger(__name__) + + +class CsvSubjectImportReader(SubjectImportReader): + def __init__(self): + self.study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] + + def load_data(self, filename): + study_subjects = [] + with open(filename) as csv_file: + reader = csv.reader(csv_file, delimiter=',') + headers = next(reader, None) + for row in reader: + subject = Subject() + study_subject = StudySubject() + study_subject.subject = subject + study_subject.study = self.study + for header, value in zip(headers, row): + self.add_data(study_subject, header, value) + if study_subject.nd_number is None or study_subject.nd_number == "": + study_subject.nd_number = study_subject.screening_number + study_subjects.append(study_subject) + return study_subjects + + def add_data(self, study_subject, column_name, value): + # type: (StudySubject, str, str) -> None + if column_name == "first_name": + study_subject.subject.first_name = self.get_new_value(study_subject.subject.first_name, column_name, value) + elif column_name == "last_name": + study_subject.subject.last_name = self.get_new_value(study_subject.subject.last_name, column_name, value) + elif column_name == "participant_id": + study_subject.screening_number = self.get_new_value(study_subject.screening_number, column_name, value) + elif column_name == "date_born": + study_subject.subject.date_born = self.get_new_date_value(study_subject.subject.date_born, column_name, + value) + else: + logger.warn("Don't know how to handle column " + column_name + " with data " + value) + + def get_new_value(self, old_value, column_name, new_value): + # type: (unicode,unicode,unicode) -> unicode + if old_value is None or old_value == "": + return new_value + if new_value is None or new_value == "": + return old_value + logger.warn( + "Contradicting entries in csv file for column: " + column_name + "(" + new_value + "," + old_value + + "). Latest value will be used") + return new_value + + def get_new_date_value(self, old_value, column_name, new_value): + # type: (datetime,unicode,unicode) -> datetime + if old_value is None or old_value == "": + try: + result = datetime.datetime.strptime(new_value, CSV_DATE_FORMAT) + except ValueError: + logger.warn("Invalid date: " + new_value) + result = old_value + return result + if new_value is None or new_value == "": + return old_value + logger.warn( + "Contradicting entries in csv file for column: " + column_name + "(" + new_value + "," + old_value + + "). Latest value will be used") + return datetime.datetime.strptime(new_value, CSV_DATE_FORMAT) diff --git a/smash/web/importer/exporter.py b/smash/web/importer/exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..99b72d79a94443e3f04f4a7ad69f2c859da7b339 --- /dev/null +++ b/smash/web/importer/exporter.py @@ -0,0 +1,50 @@ +# coding=utf-8 +import csv +import logging + +from warning_counter import MsgCounterHandler +from web.models import StudySubject + +logger = logging.getLogger(__name__) + + +class Exporter(object): + def __init__(self, filename): + # type: (str) -> None + self.filename = filename + self.exported_count = 0 + self.warning_count = 0 + + def execute(self): + self.exported_count = 0 + self.warning_count = 0 + + warning_counter = MsgCounterHandler() + logging.getLogger('').addHandler(warning_counter) + + with open(self.filename, 'w') as csv_file: + data = self.get_subjects_as_array() + writer = csv.writer(csv_file, quotechar=str(u'"'), quoting=csv.QUOTE_ALL) + for row in data: + writer.writerow([s.encode("utf-8") for s in row]) + self.exported_count += 1 + + if "WARNING" in warning_counter.level2count: + self.warning_count = warning_counter.level2count["WARNING"] + logging.getLogger('').removeHandler(warning_counter) + + def get_summary(self): + result = "<p>Number of entries: <b>" + str(self.exported_count) + "</b></p>" + style = '' + if self.warning_count > 0: + style = ' color="brown" ' + result += "<p><font " + style + ">Number of raised warnings: <b>" + str(self.warning_count) + "</b></font></p>" + + return result + + def get_subjects_as_array(self): + result = [] + study_subjects = StudySubject.objects.filter(excluded=True) + for study_subject in study_subjects: + result.append([study_subject.nd_number]) + return result diff --git a/smash/web/importer/exporter_cron_job.py b/smash/web/importer/exporter_cron_job.py new file mode 100644 index 0000000000000000000000000000000000000000..90ce95a604070e873332ec0c8610180808694ece --- /dev/null +++ b/smash/web/importer/exporter_cron_job.py @@ -0,0 +1,46 @@ +# coding=utf-8 +import logging +import traceback + +import timeout_decorator +from django.conf import settings +from django_cron import CronJobBase, Schedule + +from exporter import Exporter +from web.models.constants import CRON_JOB_TIMEOUT +from ..smash_email import EmailSender + +logger = logging.getLogger(__name__) + + +class ExporterCronJob(CronJobBase): + RUN_EVERY_MINUTES = 60 * 24 + schedule = Schedule(run_every_mins=RUN_EVERY_MINUTES) + code = 'web.import_daily_job' # a unique code + + @timeout_decorator.timeout(CRON_JOB_TIMEOUT) + def do(self): + email_title = "Daily export" + email_recipients = getattr(settings, "DEFAULT_FROM_EMAIL", None) + + filename = getattr(settings, "DAILY_EXPORT_FILE", None) + + if filename is None: + logger.info("Exporting subjects skipped. File not defined ") + return "export file not defined" + logger.info("Exporting subjects from file: " + filename) + try: + exporter = Exporter(settings.DAILY_EXPORT_FILE) + exporter.execute() + email_body = exporter.get_summary() + EmailSender().send_email(email_title, + "<h3>Data was successfully exported to file: " + filename + "</h3>" + email_body, + email_recipients) + return "export is successful" + + except: + tb = traceback.format_exc() + EmailSender().send_email(email_title, + "<h3><font color='red'>There was a problem with exporting data to file: " + filename + "</font></h3><pre>" + tb + "</pre>", + email_recipients) + return "export crashed" diff --git a/smash/web/importer/importer.py b/smash/web/importer/importer.py new file mode 100644 index 0000000000000000000000000000000000000000..2c7702f0de38fb9a29156e87b207b382f7a7b1ff --- /dev/null +++ b/smash/web/importer/importer.py @@ -0,0 +1,161 @@ +# coding=utf-8 +import logging +import sys +import traceback + +from django.conf import settings +from django.contrib.auth.models import User + +from subject_import_reader import SubjectImportReader +from warning_counter import MsgCounterHandler +from web.models import StudySubject, Subject, Provenance, Worker +from web.models.constants import GLOBAL_STUDY_ID + +logger = logging.getLogger(__name__) + + +class Importer(object): + def __init__(self, filename, reader): + # type: (str, SubjectImportReader) -> None + self.filename = filename + self.reader = reader + self.added_count = 0 + self.problematic_count = 0 + self.merged_count = 0 + self.warning_count = 0 + self.study_subjects = [] + + self.importer_user = None + + importer_user_name = getattr(settings, "IMPORTER_USER", None) + if importer_user_name is not None: + user = User.objects.filter(username=importer_user_name) + if user is None: + logger.warn("User does not exist: " + importer_user_name) + else: + self.importer_user = Worker.objects.filter(user=user) + + def execute(self): + self.added_count = 0 + self.problematic_count = 0 + self.merged_count = 0 + self.warning_count = 0 + + warning_counter = MsgCounterHandler() + logging.getLogger('').addHandler(warning_counter) + + self.study_subjects = self.reader.load_data(self.filename) + for study_subject in self.study_subjects: + try: + if study_subject.study is None: + self.problematic_count += 1 + logger.warn("Empty study found. Ignoring") + continue + elif study_subject.study.id != GLOBAL_STUDY_ID: + self.problematic_count += 1 + logger.warn("Empty study found. Ignoring: " + study_subject.study.id) + continue + else: + self.import_study_subject(study_subject) + except: + self.problematic_count += 1 + traceback.print_exc(file=sys.stdout) + logger.error("Problem with importing study subject: " + study_subject.screening_number) + if "WARNING" in warning_counter.level2count: + self.warning_count = warning_counter.level2count["WARNING"] + logging.getLogger('').removeHandler(warning_counter) + + def import_study_subject(self, study_subject): + # type: (StudySubject) -> None + db_study_subjects = StudySubject.objects.filter(screening_number=study_subject.screening_number) + if db_study_subjects.count() > 0: + db_study_subject = db_study_subjects.first() + for field in Subject._meta.get_fields(): + if field.get_internal_type() == "CharField" or field.get_internal_type() == "DateField" or field.get_internal_type() is "BooleanField": + new_value = getattr(study_subject.subject, field.name) + if new_value is not None and new_value != "": + old_value = getattr(db_study_subject.subject, field.name) + description = '{} changed from "{}" to "{}"'.format(field, old_value, new_value) + p = Provenance(modified_table=Subject._meta.db_table, + modified_table_id=db_study_subject.subject.id, + modification_author=self.importer_user, + previous_value=old_value, + new_value=new_value, + modification_description=description, + modified_field=field, + ) + setattr(db_study_subject.subject, field.name, new_value) + p.save() + + db_study_subject.subject.save() + + for field in StudySubject._meta.get_fields(): + if field.get_internal_type() == "CharField" or field.get_internal_type() == "DateField" or field.get_internal_type() is "BooleanField": + new_value = getattr(study_subject, field.name) + if new_value is not None and new_value != "": + old_value = getattr(db_study_subject, field.name) + description = '{} changed from "{}" to "{}"'.format(field, old_value, new_value) + p = Provenance(modified_table=Subject._meta.db_table, + modified_table_id=db_study_subject.id, + modification_author=self.importer_user, + previous_value=old_value, + new_value=new_value, + modification_description=description, + modified_field=field, + ) + setattr(db_study_subject, field.name, new_value) + p.save() + db_study_subject.save() + self.merged_count += 1 + else: + study_subject.subject.save() + study_subject.subject = Subject.objects.filter(id=study_subject.subject.id)[0] + study_subject.save() + + for field in Subject._meta.get_fields(): + if field.get_internal_type() == "CharField" or field.get_internal_type() == "DateField" or field.get_internal_type() is "BooleanField": + new_value = getattr(study_subject.subject, field.name) + if new_value is not None and new_value != "": + description = '{} changed from "{}" to "{}"'.format(field, '', new_value) + p = Provenance(modified_table=Subject._meta.db_table, + modified_table_id=study_subject.subject.id, + modification_author=self.importer_user, + previous_value='', + new_value=new_value, + modification_description=description, + modified_field=field, + ) + p.save() + + for field in StudySubject._meta.get_fields(): + if field.get_internal_type() == "CharField" or field.get_internal_type() == "DateField" or field.get_internal_type() is "BooleanField": + new_value = getattr(study_subject, field.name) + if new_value is not None and new_value != "": + description = '{} changed from "{}" to "{}"'.format(field, '', new_value) + p = Provenance(modified_table=Subject._meta.db_table, + modified_table_id=study_subject.id, + modification_author=self.importer_user, + previous_value='', + new_value=new_value, + modification_description=description, + modified_field=field, + ) + p.save() + + self.added_count += 1 + + def get_summary(self): + result = "<p>Number of entries: <b>" + str(len(self.study_subjects)) + "</b></p>" + \ + "<p>Number of successfully added entries: <b>" + str(self.added_count) + "</b></p>" + \ + "<p>Number of successfully merged entries: <b>" + str(self.merged_count) + "</b></p>" + style = '' + if self.problematic_count > 0: + style = ' color="red" ' + result += "<p><font " + style + ">Number of problematic entries: <b>" + str( + self.problematic_count) + "</b></font></p>" + style = '' + if self.warning_count > 0: + style = ' color="brown" ' + result += "<p><font " + style + ">Number of raised warnings: <b>" + str(self.warning_count) + "</b></font></p>" + + return result diff --git a/smash/web/importer/importer_cron_job.py b/smash/web/importer/importer_cron_job.py new file mode 100644 index 0000000000000000000000000000000000000000..bfb75d105c4d0db0daaea06034fa1a82946b182b --- /dev/null +++ b/smash/web/importer/importer_cron_job.py @@ -0,0 +1,61 @@ +# coding=utf-8 +import datetime +import logging +import os +import os.path +import traceback + +import timeout_decorator +from django.conf import settings +from django_cron import CronJobBase, Schedule + +from csv_subject_import_reader import CsvSubjectImportReader +from importer import Importer +from web.models.constants import CRON_JOB_TIMEOUT +from ..smash_email import EmailSender + +logger = logging.getLogger(__name__) + + +class ImporterCronJob(CronJobBase): + RUN_EVERY_MINUTES = 60 * 24 + schedule = Schedule(run_every_mins=RUN_EVERY_MINUTES) + code = 'web.import_daily_job' # a unique code + + @timeout_decorator.timeout(CRON_JOB_TIMEOUT) + def do(self): + email_title = "Daily import" + email_recipients = getattr(settings, "DEFAULT_FROM_EMAIL", None) + + filename = getattr(settings, "DAILY_IMPORT_FILE", None) + + if filename is None: + logger.info("Importing subjects skipped. File not defined ") + return "import file not defined" + logger.info("Importing subjects from file: " + filename) + if not os.path.isfile(filename): + EmailSender().send_email(email_title, + "<h3><font color='red'>File with imported data is not available in the system: " + filename + "</font></h3>", + email_recipients) + return "import file not found" + try: + importer = Importer(settings.DAILY_IMPORT_FILE, CsvSubjectImportReader()) + importer.execute() + email_body = importer.get_summary() + EmailSender().send_email(email_title, + "<h3>Data was successfully imported from file: " + filename + "</h3>" + email_body, + email_recipients) + self.backup_file(filename) + return "import is successful" + + except: + tb = traceback.format_exc() + EmailSender().send_email(email_title, + "<h3><font color='red'>There was a problem with importing data from file: " + filename + "</font></h3><pre>" + tb + "</pre>", + email_recipients) + return "import crashed" + + def backup_file(self, filename): + new_file = filename + "-" + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M") + ".bac" + os.rename(filename, new_file) + return diff --git a/smash/web/importer/subject_import_reader.py b/smash/web/importer/subject_import_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..0bc7f898ea24a1af7e99b32bca1c8a095635a38d --- /dev/null +++ b/smash/web/importer/subject_import_reader.py @@ -0,0 +1,7 @@ +from web.models.study_subject import StudySubject + + +class SubjectImportReader: + def load_data(self, filename): + # type: (str) -> List [StudySubject] + pass diff --git a/smash/web/importer/warning_counter.py b/smash/web/importer/warning_counter.py new file mode 100644 index 0000000000000000000000000000000000000000..8500d6a34303f5e2368f536ccd9c206069174eae --- /dev/null +++ b/smash/web/importer/warning_counter.py @@ -0,0 +1,15 @@ +import logging + +class MsgCounterHandler(logging.Handler): + level2count = None + + def __init__(self, *args, **kwargs): + super(MsgCounterHandler, self).__init__(*args, **kwargs) + self.level2count = {} + + def emit(self, record): + l = record.levelname + if l not in self.level2count: + self.level2count[l] = 0 + print(l) + self.level2count[l] += 1 diff --git a/smash/web/migrations/0148_auto_20200319_1301.py b/smash/web/migrations/0148_auto_20200319_1301.py new file mode 100644 index 0000000000000000000000000000000000000000..1b2abecbe955819c77d988944336b22ad28bd016 --- /dev/null +++ b/smash/web/migrations/0148_auto_20200319_1301.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-03-19 13:01 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('web', '0147_auto_20200320_0931'), + ] + + operations = [ + migrations.AlterModelOptions( + name='appointmenttypelink', + options={'permissions': [('view_daily_planning', 'Can see daily planning')]}, + ), + migrations.AlterField( + model_name='appointmenttype', + name='calendar_font_color', + field=models.CharField(default=b'#00000', max_length=2000, verbose_name=b'Calendar font color'), + ), + ] diff --git a/smash/web/migrations/0149_auto_20200319_1415.py b/smash/web/migrations/0149_auto_20200319_1415.py new file mode 100644 index 0000000000000000000000000000000000000000..c948e9780d3d5da309c62035ff5afb27f0d835fc --- /dev/null +++ b/smash/web/migrations/0149_auto_20200319_1415.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-03-19 14:15 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0148_auto_20200319_1301'), + ] + + operations = [ + migrations.AlterModelOptions( + name='appointment', + options={'permissions': [('send_sample_mail_for_appointments', 'Can send sample collection list')]}, + ), + ] diff --git a/smash/web/migrations/0150_auto_20200319_1446.py b/smash/web/migrations/0150_auto_20200319_1446.py new file mode 100644 index 0000000000000000000000000000000000000000..0fcdea7e40cf414c94558b82d7befe42e7dbc275 --- /dev/null +++ b/smash/web/migrations/0150_auto_20200319_1446.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-03-19 14:46 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0149_auto_20200319_1415'), + ] + + operations = [ + migrations.AlterModelOptions( + name='appointment', + options={'permissions': [('send_sample_mail_for_appointments', 'Can send sample collection list'), ('view_statistics', 'Can see statistics')]}, + ), + ] diff --git a/smash/web/migrations/0151_auto_20200319_1518.py b/smash/web/migrations/0151_auto_20200319_1518.py new file mode 100644 index 0000000000000000000000000000000000000000..a034d7b50283e5a6011cbcf3e9bf24a465c0cfcb --- /dev/null +++ b/smash/web/migrations/0151_auto_20200319_1518.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-03-19 15:18 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0150_auto_20200319_1446'), + ] + + operations = [ + migrations.AlterModelOptions( + name='subject', + options={'permissions': [('send_sample_mail_for_appointments', 'Can send sample collection list'), ('export_subjects', 'Can export subject data to excel/csv')]}, + ), + ] diff --git a/smash/web/migrations/0152_add_permissions_to_existing_workers.py b/smash/web/migrations/0152_add_permissions_to_existing_workers.py new file mode 100644 index 0000000000000000000000000000000000000000..67752e4690b59643da8f7ae2fe375db39a43f2a2 --- /dev/null +++ b/smash/web/migrations/0152_add_permissions_to_existing_workers.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-03-19 13:01 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('web', '0151_auto_20200319_1518'), + ] + + operations = [ + migrations.RunSQL("insert into web_workerstudyrole_permissions(workerstudyrole_id, permission_id) " + "select web_workerstudyrole.id, auth_permission.id from web_workerstudyrole,auth_permission " + "where codename='view_daily_planning';"), + migrations.RunSQL("insert into web_workerstudyrole_permissions(workerstudyrole_id, permission_id) " + "select web_workerstudyrole.id, auth_permission.id from web_workerstudyrole,auth_permission " + "where codename='change_flyingteam';"), + migrations.RunSQL("insert into web_workerstudyrole_permissions(workerstudyrole_id, permission_id) " + "select web_workerstudyrole.id, auth_permission.id from web_workerstudyrole,auth_permission " + "where codename='export_subjects';"), + + ] diff --git a/smash/web/migrations/0153_auto_20200320_0932.py b/smash/web/migrations/0153_auto_20200320_0932.py new file mode 100644 index 0000000000000000000000000000000000000000..06a7de106240cf856fceb9720477be0b9aa87526 --- /dev/null +++ b/smash/web/migrations/0153_auto_20200320_0932.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-03-20 09:32 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0152_add_permissions_to_existing_workers'), + ] + + operations = [ + migrations.AddField( + model_name='studycolumns', + name='brain_donation_agreement', + field=models.BooleanField(default=False, verbose_name=b'Brain donation agreement'), + ), + migrations.AddField( + model_name='studysubject', + name='brain_donation_agreement', + field=models.BooleanField(default=False, verbose_name=b'Brain donation agreement'), + ), + migrations.AddField( + model_name='subject', + name='next_of_keen_address', + field=models.TextField(blank=True, max_length=2000, verbose_name=b'Next of keen address'), + ), + migrations.AddField( + model_name='subject', + name='next_of_keen_name', + field=models.CharField(blank=True, max_length=50, verbose_name=b'Next of keen'), + ), + migrations.AddField( + model_name='subject', + name='next_of_keen_phone', + field=models.CharField(blank=True, max_length=50, verbose_name=b'Next of keen phone'), + ), + migrations.AddField( + model_name='subjectcolumns', + name='next_of_keen_address', + field=models.BooleanField(default=False, max_length=1, verbose_name=b'Next of keen address'), + ), + migrations.AddField( + model_name='subjectcolumns', + name='next_of_keen_name', + field=models.BooleanField(default=False, max_length=1, verbose_name=b'Next of keen'), + ), + migrations.AddField( + model_name='subjectcolumns', + name='next_of_keen_phone', + field=models.BooleanField(default=False, max_length=1, verbose_name=b'Next of keen phone'), + ), + ] diff --git a/smash/web/migrations/0154_add_permission_to_existing_workers.py b/smash/web/migrations/0154_add_permission_to_existing_workers.py new file mode 100644 index 0000000000000000000000000000000000000000..76d2ca30d1e461f10b09a110f1b3d66d036dff4f --- /dev/null +++ b/smash/web/migrations/0154_add_permission_to_existing_workers.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-03-19 13:01 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('web', '0153_auto_20200320_0932'), + ] + + operations = [ + migrations.RunSQL("insert into web_workerstudyrole_permissions(workerstudyrole_id, permission_id) " + "select web_workerstudyrole.id, auth_permission.id from web_workerstudyrole,auth_permission " + "where codename='add_subject';"), + + ] diff --git a/smash/web/migrations/0150_auto_20200406_1144.py b/smash/web/migrations/0155_auto_20200406_1144.py similarity index 86% rename from smash/web/migrations/0150_auto_20200406_1144.py rename to smash/web/migrations/0155_auto_20200406_1144.py index c0c35fb793fd5fbd0d4ae261105f7f7c02e4620b..438315eef6d19f38f08b0f8e04255c59bd2ad612 100644 --- a/smash/web/migrations/0150_auto_20200406_1144.py +++ b/smash/web/migrations/0155_auto_20200406_1144.py @@ -8,7 +8,7 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('web', '0147_auto_20200320_0931'), + ('web', '0154_add_permission_to_existing_workers'), ] operations = [ diff --git a/smash/web/migrations/0151_auto_20200406_1207.py b/smash/web/migrations/0156_auto_20200406_1207.py similarity index 90% rename from smash/web/migrations/0151_auto_20200406_1207.py rename to smash/web/migrations/0156_auto_20200406_1207.py index 630fa020bc600198b67ec8b76aa5379222e6426a..dc0544dbc712148c6d38ad9e14c7eb29ba92541a 100644 --- a/smash/web/migrations/0151_auto_20200406_1207.py +++ b/smash/web/migrations/0156_auto_20200406_1207.py @@ -8,7 +8,7 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('web', '0150_auto_20200406_1144'), + ('web', '0155_auto_20200406_1144'), ] operations = [ diff --git a/smash/web/models/appointment.py b/smash/web/models/appointment.py index 9b5b3944c623d47a8e090bccab83fbfed6eb245b..c7a52d416f21c313e7930088f85944c91f35cd50 100644 --- a/smash/web/models/appointment.py +++ b/smash/web/models/appointment.py @@ -11,6 +11,10 @@ from . import ConfigurationItem class Appointment(models.Model): class Meta: app_label = 'web' + permissions = [ + ("send_sample_mail_for_appointments", "Can send sample collection list"), + ("view_statistics", "Can see statistics"), + ] APPOINTMENT_STATUS_SCHEDULED = 'SCHEDULED' APPOINTMENT_STATUS_FINISHED = 'FINISHED' diff --git a/smash/web/models/appointment_type_link.py b/smash/web/models/appointment_type_link.py index a48a55026880f9437ee66c390a19645dfe3a5671..c2de6050854dc57820b72b3d53b3f945f3d08aaf 100644 --- a/smash/web/models/appointment_type_link.py +++ b/smash/web/models/appointment_type_link.py @@ -2,6 +2,10 @@ from django.db import models class AppointmentTypeLink(models.Model): + class Meta: + permissions = [ + ("view_daily_planning", "Can see daily planning"), + ] appointment = models.ForeignKey("web.Appointment", on_delete=models.CASCADE) appointment_type = models.ForeignKey("web.AppointmentType", on_delete=models.CASCADE) date_when = models.DateTimeField(null=True, default=None) diff --git a/smash/web/models/study_columns.py b/smash/web/models/study_columns.py index a1b345700db1db01741b050496d1e609524c30da..7d20b3913d40a71f72fc0b70327a20084e83a158 100644 --- a/smash/web/models/study_columns.py +++ b/smash/web/models/study_columns.py @@ -1,8 +1,6 @@ # coding=utf-8 from django.db import models -from web.models.constants import BOOL_CHOICES - class StudyColumns(models.Model): class Meta: @@ -81,7 +79,7 @@ class StudyColumns(models.Model): excluded = models.BooleanField(default=False, verbose_name='Excluded') endpoint_reached = models.BooleanField(default=True, verbose_name='Endpoint reached') - + resign_reason = models.BooleanField(default=True, verbose_name='Endpoint reached comments') referral_letter = models.BooleanField( @@ -117,4 +115,9 @@ class StudyColumns(models.Model): vouchers = models.BooleanField( default=False, verbose_name='Vouchers', - ) \ No newline at end of file + ) + + brain_donation_agreement = models.BooleanField( + default=False, + verbose_name='Brain donation agreement', + ) diff --git a/smash/web/models/study_subject.py b/smash/web/models/study_subject.py index 0fe4b5c4129a7c3952aefca764dca658f3e2920f..22a5b0b8fe77518abb0092587531f0b715413633 100644 --- a/smash/web/models/study_subject.py +++ b/smash/web/models/study_subject.py @@ -167,6 +167,11 @@ class StudySubject(models.Model): verbose_name='PD in family', default=None, ) + brain_donation_agreement = models.BooleanField( + default=False, + verbose_name='Brain donation agreement', + ) + resigned = models.BooleanField( verbose_name='Resigned', default=False, diff --git a/smash/web/models/subject.py b/smash/web/models/subject.py index 5a323cbe8a243f64182291dc01c657dd3a30847b..918d4645513b2a690bb163d79c8b448b741dc141 100644 --- a/smash/web/models/subject.py +++ b/smash/web/models/subject.py @@ -1,20 +1,25 @@ # coding=utf-8 import logging + from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver from constants import SEX_CHOICES, COUNTRY_OTHER_ID from web.models import Country, Visit, Appointment, Provenance from . import Language -from django.db.models.signals import post_save -from django.dispatch import receiver logger = logging.getLogger(__name__) -class Subject(models.Model): +class Subject(models.Model): class Meta: app_label = 'web' - + permissions = [ + ("send_sample_mail_for_appointments", "Can send sample collection list"), + ("export_subjects", "Can export subject data to excel/csv"), + ] + @property def provenances(self): return Provenance.objects.filter(modified_table=Subject._meta.db_table, modified_table_id=self.id).order_by('-modification_date') @@ -100,6 +105,21 @@ class Subject(models.Model): verbose_name='Country' ) + next_of_keen_name = models.CharField(max_length=50, + blank=True, + verbose_name='Next of keen' + ) + + next_of_keen_phone = models.CharField(max_length=50, + blank=True, + verbose_name='Next of keen phone' + ) + + next_of_keen_address = models.TextField(max_length=2000, + blank=True, + verbose_name='Next of keen address' + ) + dead = models.BooleanField( verbose_name='Deceased', default=False, @@ -107,7 +127,7 @@ class Subject(models.Model): ) def pretty_address(self): - return u'{} ({}), {}. {}'.format(self.address, self.postal_code, self.city, self.country) + return u'{} ({}), {}. {}'.format(self.address, self.postal_code, self.city, self.country) def mark_as_dead(self): self.dead = True @@ -134,7 +154,7 @@ class Subject(models.Model): return "%s %s" % (self.first_name, self.last_name) -#SIGNALS +# SIGNALS @receiver(post_save, sender=Subject) def set_as_deceased(sender, instance, **kwargs): if instance.dead: diff --git a/smash/web/models/subject_columns.py b/smash/web/models/subject_columns.py index d83847c7d6589aa98adbcd1740137fcb1a80526b..04a1869b0787f66eb61e8c76bd858419db16798f 100644 --- a/smash/web/models/subject_columns.py +++ b/smash/web/models/subject_columns.py @@ -83,3 +83,18 @@ class SubjectColumns(models.Model): default=True, verbose_name='Deceased', ) + + next_of_keen_name = models.BooleanField(max_length=1, + default=False, + verbose_name='Next of keen', + ) + + next_of_keen_phone = models.BooleanField(max_length=1, + default=False, + verbose_name='Next of keen phone', + ) + + next_of_keen_address = models.BooleanField(max_length=1, + default=False, + verbose_name='Next of keen address', + ) diff --git a/smash/web/templates/_base.html b/smash/web/templates/_base.html index da7b7ddab5d8ed1c554095a1dff663a440158df7..ecc810b2bced77a1747bfe65c65e711ff1933541 100644 --- a/smash/web/templates/_base.html +++ b/smash/web/templates/_base.html @@ -260,11 +260,11 @@ desired effect {% block footer %} <!-- To the right --> <div class="pull-right hidden-xs"> - Version: <strong>0.13.1</strong> (9 Apr 2019) + Version: <strong>0.14.0</strong> (1 Apr 2020) </div> <!-- Default to the left --> - 2019, Parkinson Research Clinic <!--(eg. <small> + 2020, Parkinson Research Clinic <!--(eg. <small> <strong>Copyright © 2016 <a href="#">Company</a>.</strong> All rights reserved. </small>)--> diff --git a/smash/web/templates/sidebar.html b/smash/web/templates/sidebar.html index a91be3d75937d828cecab8216c779c3b557271a7..a575319957dc7bab0a8d2cf03b4204c9796d53b3 100644 --- a/smash/web/templates/sidebar.html +++ b/smash/web/templates/sidebar.html @@ -16,12 +16,14 @@ </a> </li> - <li data-desc="daily_planning"> - <a href="{% url 'web.views.daily_planning' %}"> - <i class="fa fa-clock-o"></i> - <span>Daily Planning</span> - </a> - </li> + {% if "view_daily_planning" in permissions %} + <li data-desc="daily_planning"> + <a href="{% url 'web.views.daily_planning' %}"> + <i class="fa fa-clock-o"></i> + <span>Daily Planning</span> + </a> + </li> + {% endif %} {% if "change_worker" in permissions %} <li data-desc="workers"> @@ -32,44 +34,63 @@ </li> {% endif %} - <li data-desc="equipment_and_rooms" class="treeview"> - <a href="{% url 'web.views.equipment_and_rooms' %}"> - <i class="fa fa-building-o"></i> <span>Equipment & Rooms</span> - <span class="pull-right-container"> + {% if equipment_perms %} + <li data-desc="equipment_and_rooms" class="treeview"> + <a href="{% url 'web.views.equipment_and_rooms' %}"> + <i class="fa fa-building-o"></i> <span>Equipment & Rooms</span> + <span class="pull-right-container"> <i class="fa fa-angle-left pull-right"></i> </span> - </a> - <ul class="treeview-menu"> - <li data-desc="equipment_items"><a href="{% url 'web.views.equipment' %}">Equipment items</a></li> - {% if "change_appointmenttype" in permissions %} - <li data-desc="appointment_types"><a href="{% url 'web.views.appointment_types' %}">Appointment Types</a></li> - {% endif %} - <li data-desc="flying_teams"><a href="{% url 'web.views.equipment_and_rooms.flying_teams' %}">Flying teams</a></li> - <li data-desc="kit_requests"><a href="{% url 'web.views.kit_requests' %}">Kit requests</a></li> - <li data-desc="rooms"><a href="{% url 'web.views.equipment_and_rooms.rooms' %}">Rooms</a></li> - </ul> - </li> + </a> + <ul class="treeview-menu"> + {% if "change_item" in permissions %} + <li data-desc="equipment_items"><a href="{% url 'web.views.equipment' %}">Equipment items</a></li> + {% endif %} + {% if "change_appointmenttype" in permissions %} + <li data-desc="appointment_types"><a href="{% url 'web.views.appointment_types' %}">Appointment + Types</a></li> + {% endif %} + {% if "change_flyingteam" in permissions %} + <li data-desc="flying_teams"><a href="{% url 'web.views.equipment_and_rooms.flying_teams' %}">Flying + teams</a></li> + {% endif %} + {% if "send_sample_mail_for_appointments" in permissions %} + <li data-desc="kit_requests"><a href="{% url 'web.views.kit_requests' %}">Kit requests</a></li> + {% endif %} + {% if "change_room" in permissions %} + <li data-desc="rooms"><a href="{% url 'web.views.equipment_and_rooms.rooms' %}">Rooms</a></li> + {% endif %} + </ul> + </li> + {% endif %} - <li data-desc="statistics"> - <a href="{% url 'web.views.statistics' %}"> - <i class="fa fa-bar-chart" aria-hidden="true"></i> - <span>Statistics</span> - </a> - </li> + {% if "view_statistics" in permissions %} + <li data-desc="statistics"> + <a href="{% url 'web.views.statistics' %}"> + <i class="fa fa-bar-chart" aria-hidden="true"></i> + <span>Statistics</span> + </a> + </li> + {% endif %} - <li data-desc="mail_templates"> - <a href="{% url 'web.views.mail_templates' %}"> - <i class="fa fa-envelope-o"></i> - <span>Mail templates</span> - </a> - </li> - <li data-desc="export"> - <a href="{% url 'web.views.export' %}"> - <i class="fa fa-file-excel-o"></i> - <span>Export</span> - </a> - </li> + {% if "change_mailtemplate" in permissions %} + <li data-desc="mail_templates"> + <a href="{% url 'web.views.mail_templates' %}"> + <i class="fa fa-envelope-o"></i> + <span>Mail templates</span> + </a> + </li> + {% endif %} + + {% if "export_subjects" in permissions %} + <li data-desc="export"> + <a href="{% url 'web.views.export' %}"> + <i class="fa fa-file-excel-o"></i> + <span>Export</span> + </a> + </li> + {% endif %} {% if study.has_vouchers and "change_voucher" in permissions%} <li data-desc="vouchers"> diff --git a/smash/web/templates/subjects/edit.html b/smash/web/templates/subjects/edit.html index 836dfaf324d6af64c22227985ca7f0ea856668df..eb668516c1bce8ed011ffda5715dff29112b8cbf 100644 --- a/smash/web/templates/subjects/edit.html +++ b/smash/web/templates/subjects/edit.html @@ -223,6 +223,11 @@ $("#confirm-dead-resigned-mark-dialog").modal("show"); return false; } + var brainDonation = $("#id_study_subject-brain_donation_agreement").is(":checked"); + if (brainDonation && ($("#id_subject-next_of_keen_phone").val() === '' || $("#id_subject-next_of_keen_address").val() === '' || $("#id_subject-next_of_keen_name").val() === '')) { + alert("Next of keen data must be entered when brain donation agreement is in place"); + return false; + } }); $("#confirm-save").click(function () { confirmed = true; diff --git a/smash/web/templates/subjects/index.html b/smash/web/templates/subjects/index.html index cfe954bcc1251c81a64dffee7c12a18b62893bda..29a4d2308145826a49e5d49091eb949a3157b524 100644 --- a/smash/web/templates/subjects/index.html +++ b/smash/web/templates/subjects/index.html @@ -31,7 +31,7 @@ padding-left: 2px; } .visit_row > span > a{ - color: inherit; + color: inherit; } .appointment_type_list{ margin-top: 10px; @@ -57,7 +57,11 @@ {% block maincontent %} <div> - <a href="{% url 'web.views.subject_add' %}" class="btn btn-app"> + <a href="{% url 'web.views.subject_add' %}" class="btn btn-app" + {% if not "add_subject" in permissions %} + disabled + {% endif %} + > <i class="fa fa-plus"></i> Add new subject </a> diff --git a/smash/web/tests/data/import.csv b/smash/web/tests/data/import.csv new file mode 100644 index 0000000000000000000000000000000000000000..b13e437a14950691eb4f1daaa7b9cc2a609f8843 --- /dev/null +++ b/smash/web/tests/data/import.csv @@ -0,0 +1,2 @@ +first_name,last_name,participant_id,date_born +Piotr,Gawron,Cov-000001,01-02-2020 \ No newline at end of file diff --git a/smash/web/tests/data/import_date_of_birth.csv b/smash/web/tests/data/import_date_of_birth.csv new file mode 100644 index 0000000000000000000000000000000000000000..43d9adb8ac452c865ffcbdb96446f22847873c61 --- /dev/null +++ b/smash/web/tests/data/import_date_of_birth.csv @@ -0,0 +1,4 @@ +first_name,last_name,participant_id,date_born +Piotr,Gawron,Cov-000001, +Piotr,Gawron,Cov-000002,invalid +Piotr,Gawron,Cov-000003,2222-20-20 \ No newline at end of file diff --git a/smash/web/tests/importer/__init__.py b/smash/web/tests/importer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/smash/web/tests/importer/mock_reader.py b/smash/web/tests/importer/mock_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..a5aedb09dd658512fcbcfbe8a80913e6f144026f --- /dev/null +++ b/smash/web/tests/importer/mock_reader.py @@ -0,0 +1,13 @@ +import logging + +from web.importer.subject_import_reader import SubjectImportReader + +logger = logging.getLogger(__name__) + + +class MockReader(SubjectImportReader): + def __init__(self, study_subjects): + self.study_subjects = study_subjects + + def load_data(self, filename): + return self.study_subjects diff --git a/smash/web/tests/importer/test_csv_subject_import_reader.py b/smash/web/tests/importer/test_csv_subject_import_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..a80f323469577c13837ee6bf83c6ed1baebd004c --- /dev/null +++ b/smash/web/tests/importer/test_csv_subject_import_reader.py @@ -0,0 +1,37 @@ +# coding=utf-8 + +import logging + +from django.test import TestCase + +from web.importer import CsvSubjectImportReader +from web.tests.functions import get_resource_path + +logger = logging.getLogger(__name__) + + +class TestCsvReader(TestCase): + + def test_load_data(self): + filename = get_resource_path('import.csv') + study_subjects = CsvSubjectImportReader().load_data(filename) + self.assertEqual(1, len(study_subjects)) + study_subject = study_subjects[0] + self.assertEqual("Piotr", study_subject.subject.first_name) + self.assertEqual("Gawron", study_subject.subject.last_name) + self.assertEqual("Cov-000001", study_subject.screening_number) + self.assertEqual("Cov-000001", study_subject.nd_number) + + self.assertEqual(1, study_subject.subject.date_born.day) + self.assertEqual(2, study_subject.subject.date_born.month) + self.assertEqual(2020, study_subject.subject.date_born.year) + + self.assertIsNotNone(study_subject.study) + + def test_load_problematic_dates(self): + filename = get_resource_path('import_date_of_birth.csv') + study_subjects = CsvSubjectImportReader().load_data(filename) + self.assertEqual(3, len(study_subjects)) + self.assertIsNone(study_subjects[0].subject.date_born) + self.assertIsNone(study_subjects[1].subject.date_born) + self.assertIsNone(study_subjects[2].subject.date_born) diff --git a/smash/web/tests/importer/test_exporter.py b/smash/web/tests/importer/test_exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..3a552443b9ba73818783f213a5af0ced1b50a890 --- /dev/null +++ b/smash/web/tests/importer/test_exporter.py @@ -0,0 +1,40 @@ +# coding=utf-8 + +import datetime +import logging + +from django.test import TestCase + +from web.tests.functions import create_study_subject +from web.importer import Exporter +from web.models import Subject, StudySubject, Study, Provenance +from web.models.constants import GLOBAL_STUDY_ID + +logger = logging.getLogger(__name__) + + +class TestExporter(TestCase): + + def setUp(self): + self.study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] + + def test_export_not_excluded(self): + create_study_subject() + + exporter = Exporter(filename="empty.csv") + exporter.execute() + + self.assertEqual(0, exporter.exported_count) + self.assertEqual(0, exporter.warning_count) + + def test_export_excluded(self): + subject = create_study_subject() + subject.excluded=True + subject.save() + + exporter = Exporter(filename="empty.csv") + exporter.execute() + + self.assertEqual(1, exporter.exported_count) + self.assertEqual(0, exporter.warning_count) + diff --git a/smash/web/tests/importer/test_exporter_cron_job.py b/smash/web/tests/importer/test_exporter_cron_job.py new file mode 100644 index 0000000000000000000000000000000000000000..d1b44a37fbb2eec72ddceeef88d43d2928f689c4 --- /dev/null +++ b/smash/web/tests/importer/test_exporter_cron_job.py @@ -0,0 +1,45 @@ +# coding=utf-8 + +import logging +import tempfile + +from django.conf import settings +from django.test import TestCase + +from web.importer import ExporterCronJob + +logger = logging.getLogger(__name__) +from django.core import mail +from django_cron.models import CronJobLog + + +class TestCronJobExporter(TestCase): + + def setUp(self): + setattr(settings, "DAILY_EXPORT_FILE", None) + + def tearDown(self): + setattr(settings, "DAILY_EXPORT_FILE", None) + + def test_import_without_configuration(self): + CronJobLog.objects.all().delete() + + job = ExporterCronJob() + + status = job.do() + + self.assertEqual("export file not defined", status) + self.assertEqual(0, len(mail.outbox)) + + def test_import(self): + new_file, tmp = tempfile.mkstemp() + + setattr(settings, "DAILY_EXPORT_FILE", tmp) + CronJobLog.objects.all().delete() + + job = ExporterCronJob() + + status = job.do() + + self.assertEqual("export is successful", status) + self.assertEqual(1, len(mail.outbox)) diff --git a/smash/web/tests/importer/test_importer.py b/smash/web/tests/importer/test_importer.py new file mode 100644 index 0000000000000000000000000000000000000000..a5e95a1ea900a4a1742abece15e00cfea13160d2 --- /dev/null +++ b/smash/web/tests/importer/test_importer.py @@ -0,0 +1,110 @@ +# coding=utf-8 + +import datetime +import logging + +from django.test import TestCase + +from mock_reader import MockReader +from web.tests.functions import create_study_subject +from web.importer import Importer +from web.models import Subject, StudySubject, Study, Provenance +from web.models.constants import GLOBAL_STUDY_ID + +logger = logging.getLogger(__name__) + + +class TestImporter(TestCase): + + def setUp(self): + self.study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] + + def test_import_new_subject(self): + study_subjects = [] + subject = Subject() + subject.first_name = "Piotr" + subject.last_name = "Gawron" + study_subject = StudySubject() + study_subject.screening_number = "Cov-123456" + study_subject.subject = subject + study_subject.study = self.study + study_subjects.append(study_subject) + + importer = Importer(filename="empty.csv", reader=MockReader(study_subjects)) + subject_counter = Subject.objects.count() + study_subject_counter = StudySubject.objects.count() + provenance_counter = Provenance.objects.count() + importer.execute() + + self.assertEqual(subject_counter + 1, Subject.objects.count()) + self.assertEqual(study_subject_counter + 1, StudySubject.objects.count()) + self.assertNotEqual(provenance_counter, Provenance.objects.count()) + + self.assertEqual(1, importer.added_count) + self.assertEqual(0, importer.problematic_count) + self.assertEqual(0, importer.warning_count) + + def test_import_invalid(self): + study_subjects = [] + study_subject = StudySubject() + study_subject.screening_number = "Cov-123456" + study_subject.study = self.study + study_subjects.append(study_subject) + + importer = Importer(filename="empty.csv", reader=MockReader(study_subjects)) + study_subject_counter = StudySubject.objects.count() + importer.execute() + + self.assertEqual(study_subject_counter, StudySubject.objects.count()) + + self.assertEqual(0, importer.added_count) + self.assertEqual(1, importer.problematic_count) + + def test_import_no_study(self): + study_subjects = [] + study_subject = StudySubject() + study_subject.screening_number = "Cov-123456" + study_subjects.append(study_subject) + + importer = Importer(filename="empty.csv", reader=MockReader(study_subjects)) + study_subject_counter = StudySubject.objects.count() + importer.execute() + + self.assertEqual(study_subject_counter, StudySubject.objects.count()) + + self.assertEqual(0, importer.added_count) + self.assertEqual(1, importer.problematic_count) + + def test_import_merge_subject(self): + existing_study_subject = create_study_subject() + study_subjects = [] + subject = Subject() + subject.first_name = "XYZ" + subject.last_name = "AAA" + subject.date_born = datetime.datetime.now() + study_subject = StudySubject() + study_subject.screening_number = existing_study_subject.screening_number + study_subject.subject = subject + study_subject.study = self.study + study_subjects.append(study_subject) + + importer = Importer(filename="empty.csv", reader=MockReader(study_subjects)) + provenance_counter = Provenance.objects.count() + subject_counter = Subject.objects.count() + study_subject_counter = StudySubject.objects.count() + importer.execute() + + self.assertEqual(subject_counter, Subject.objects.count()) + self.assertEqual(study_subject_counter, StudySubject.objects.count()) + self.assertNotEqual(provenance_counter, Provenance.objects.count()) + + self.assertEqual(0, importer.added_count) + self.assertEqual(1, importer.merged_count) + self.assertEqual(0, importer.problematic_count) + self.assertEqual(0, importer.warning_count) + + existing_study_subject = StudySubject.objects.filter(id=existing_study_subject.id)[0] + self.assertEquals(existing_study_subject.subject.first_name, subject.first_name) + self.assertEquals(existing_study_subject.subject.last_name, subject.last_name) + self.assertEquals(existing_study_subject.subject.date_born.strftime("%Y-%m-%d"), + subject.date_born.strftime("%Y-%m-%d")) diff --git a/smash/web/tests/importer/test_importer_cron_job.py b/smash/web/tests/importer/test_importer_cron_job.py new file mode 100644 index 0000000000000000000000000000000000000000..0eec9f6acb0969b11f703639ee961f665b71ad13 --- /dev/null +++ b/smash/web/tests/importer/test_importer_cron_job.py @@ -0,0 +1,46 @@ +# coding=utf-8 + +import logging +import tempfile +from shutil import copyfile + +from django.conf import settings +from django.test import TestCase + +from web.importer import ImporterCronJob +from web.tests.functions import get_resource_path + +logger = logging.getLogger(__name__) +from django.core import mail +from django_cron.models import CronJobLog + + +class TestCronJobImporter(TestCase): + + def tearDown(self): + setattr(settings, "DAILY_IMPORT_FILE", None) + + def test_import_without_configuration(self): + CronJobLog.objects.all().delete() + + job = ImporterCronJob() + + status = job.do() + + self.assertEqual("import file not defined", status) + self.assertEqual(0, len(mail.outbox)) + + def test_import(self): + filename = get_resource_path('import.csv') + new_file, tmp = tempfile.mkstemp() + copyfile(filename, tmp) + + setattr(settings, "DAILY_IMPORT_FILE", tmp) + CronJobLog.objects.all().delete() + + job = ImporterCronJob() + + status = job.do() + + self.assertEqual("import is successful", status) + self.assertEqual(1, len(mail.outbox)) diff --git a/smash/web/tests/view/test_daily_planning.py b/smash/web/tests/view/test_daily_planning.py new file mode 100644 index 0000000000000000000000000000000000000000..096cc9200a770d92d5665aa33ff5cb94ac08635e --- /dev/null +++ b/smash/web/tests/view/test_daily_planning.py @@ -0,0 +1,20 @@ +import logging + +from django.urls import reverse + +from web.tests import LoggedInTestCase + +logger = logging.getLogger(__name__) + + +class DailyPlanningViewTests(LoggedInTestCase): + def test_visit_details_request(self): + self.login_as_admin() + response = self.client.get(reverse('web.views.daily_planning')) + + self.assertEqual(response.status_code, 200) + + def test_visit_details_request_without_permissions(self): + self.login_as_staff() + response = self.client.get(reverse('web.views.daily_planning')) + self.assertEqual(response.status_code, 302) diff --git a/smash/web/tests/view/test_equipments.py b/smash/web/tests/view/test_equipments.py index 99e312a8c85d747a66ca3a90396f1c9ece48b9f5..34cba24a89c5595b000dbec28d2f36e967c8f223 100644 --- a/smash/web/tests/view/test_equipments.py +++ b/smash/web/tests/view/test_equipments.py @@ -10,7 +10,14 @@ logger = logging.getLogger(__name__) class EquipmentTests(LoggedInTestCase): + + def test_list_without_permissions(self): + self.login_as_staff() + response = self.client.get(reverse('web.views.equipment')) + self.assertEqual(response.status_code, 302) + def test_equipment_requests(self): + self.login_as_admin() pages = [ 'web.views.equipment', 'web.views.equipment_add', @@ -21,6 +28,7 @@ class EquipmentTests(LoggedInTestCase): self.assertEqual(response.status_code, 200) def test_equipment_edit_request(self): + self.login_as_admin() item = create_item() page = reverse('web.views.equipment_edit', kwargs={'equipment_id': str(item.id)}) @@ -28,6 +36,7 @@ class EquipmentTests(LoggedInTestCase): self.assertEqual(response.status_code, 200) def test_equipment_delete_request(self): + self.login_as_admin() item = create_item() page = reverse('web.views.equipment_delete', kwargs={'equipment_id': str(item.id)}) @@ -35,6 +44,7 @@ class EquipmentTests(LoggedInTestCase): self.assertEqual(response.status_code, 302) def test_equipment_add(self): + self.login_as_admin() page = reverse('web.views.equipment_add') data = { 'name': 'The mysterious potion', @@ -48,6 +58,7 @@ class EquipmentTests(LoggedInTestCase): self.assertEqual(len(freshly_created), 1) def test_equipment_edit(self): + self.login_as_admin() item = create_item() page = reverse('web.views.equipment_edit', kwargs={'equipment_id': str(item.id)}) @@ -64,6 +75,7 @@ class EquipmentTests(LoggedInTestCase): self.assertEqual(getattr(freshly_edited, key, ''), data[key]) def test_equipment_delete(self): + self.login_as_admin() item = create_item() page = reverse('web.views.equipment_delete', kwargs={'equipment_id': str(item.id)}) diff --git a/smash/web/tests/view/test_export.py b/smash/web/tests/view/test_export.py index d6938476f7fd4ea6e3d3b36f2441e448fb02bfad..3f6a5c99b7fc12060eb0aad8ca95a92460d424de 100644 --- a/smash/web/tests/view/test_export.py +++ b/smash/web/tests/view/test_export.py @@ -9,26 +9,47 @@ from web.views.export import subject_to_row_for_fields, DROP_OUT_FIELD class TestExportView(LoggedInTestCase): def test_export_subjects_to_csv(self): + self.login_as_admin() create_study_subject() response = self.client.get(reverse('web.views.export_to_csv', kwargs={'data_type': "subjects"})) self.assertEqual(response.status_code, 200) + def test_export_subjects_to_csv_without_permission(self): + response = self.client.get(reverse("web.views.mail_templates")) + create_study_subject() + response = self.client.get(reverse('web.views.export_to_csv', kwargs={'data_type': "subjects"})) + self.assertEqual(response.status_code, 302) + def test_render_export(self): + self.login_as_admin() create_study_subject() response = self.client.get(reverse('web.views.export')) self.assertEqual(response.status_code, 200) + def test_render_export_without_permission(self): + create_study_subject() + response = self.client.get(reverse('web.views.export')) + self.assertEqual(response.status_code, 302) + def test_export_appointments_to_csv(self): + self.login_as_admin() create_appointment() response = self.client.get(reverse('web.views.export_to_csv', kwargs={'data_type': "appointments"})) self.assertEqual(response.status_code, 200) def test_export_subjects_to_excel(self): + self.login_as_admin() create_study_subject() response = self.client.get(reverse('web.views.export_to_excel', kwargs={'data_type': "subjects"})) self.assertEqual(response.status_code, 200) + def test_export_subjects_to_excel_without_permission(self): + create_study_subject() + response = self.client.get(reverse('web.views.export_to_excel', kwargs={'data_type': "subjects"})) + self.assertEqual(response.status_code, 302) + def test_export_appointments_to_excel(self): + self.login_as_admin() appointment = create_appointment() appointment.visit = None appointment.save() diff --git a/smash/web/tests/view/test_flying_teams.py b/smash/web/tests/view/test_flying_teams.py index 32cfe22ff9d0ac7cbd073f66dd3691ff051a06b9..e282c3e9cc14987d9c4a4a7ca5b90ca26af1c907 100644 --- a/smash/web/tests/view/test_flying_teams.py +++ b/smash/web/tests/view/test_flying_teams.py @@ -17,6 +17,7 @@ class FlyingTeamTests(LoggedInTestCase): return 'Random' + ''.join(random.choice(letters) for x in range(15)) def test_flying_team_requests(self): + self.login_as_admin() pages = [ 'web.views.equipment_and_rooms.flying_teams', 'web.views.equipment_and_rooms.flying_teams_add', @@ -26,7 +27,18 @@ class FlyingTeamTests(LoggedInTestCase): response = self.client.get(reverse(page)) self.assertEqual(response.status_code, 200) + def test_flying_team_requests_without_permission(self): + pages = [ + 'web.views.equipment_and_rooms.flying_teams', + 'web.views.equipment_and_rooms.flying_teams_add', + ] + + for page in pages: + response = self.client.get(reverse(page)) + self.assertEqual(response.status_code, 302) + def test_flying_team_add(self): + self.login_as_admin() page = reverse('web.views.equipment_and_rooms.flying_teams_add') data = { 'place': self.generate_more_or_less_random_name() @@ -38,6 +50,7 @@ class FlyingTeamTests(LoggedInTestCase): self.assertEqual(len(freshly_created), 1) def test_flying_team_edit(self): + self.login_as_admin() flying_team = create_flying_team() page = reverse('web.views.equipment_and_rooms.flying_teams_edit', kwargs={'flying_team_id': str(flying_team.id)}) @@ -51,6 +64,7 @@ class FlyingTeamTests(LoggedInTestCase): self.assertEqual(freshly_edited.place, data["place"]) def test_flying_team_edit_request(self): + self.login_as_admin() flying_team = create_flying_team() page = reverse('web.views.equipment_and_rooms.flying_teams_edit', kwargs={'flying_team_id': str(flying_team.id)}) diff --git a/smash/web/tests/view/test_kit_request.py b/smash/web/tests/view/test_kit_request.py index 387001f3bd804c87fef7db3dccac4de4897dd97f..72222f5df0f4de62743ed4d3edbf388f8232b008 100644 --- a/smash/web/tests/view/test_kit_request.py +++ b/smash/web/tests/view/test_kit_request.py @@ -5,7 +5,8 @@ from django.urls import reverse from web.models import Item, Appointment, AppointmentTypeLink from web.tests import LoggedInTestCase -from web.tests.functions import create_appointment_type, create_appointment, create_visit, create_appointment_without_visit +from web.tests.functions import create_appointment_type, create_appointment, create_visit, \ + create_appointment_without_visit from web.views.kit import get_kit_requests from web.views.notifications import get_today_midnight_date @@ -13,10 +14,16 @@ from web.views.notifications import get_today_midnight_date class ViewFunctionsTests(LoggedInTestCase): def test_kit_requests(self): + self.login_as_admin() response = self.client.get(reverse('web.views.kit_requests')) self.assertEqual(response.status_code, 200) + def test_kit_requests_without_permission(self): + response = self.client.get(reverse('web.views.kit_requests')) + self.assertEqual(response.status_code, 302) + def test_kit_requests_2(self): + self.login_as_admin() item_name = "Test item to be ordered" item = Item.objects.create(disposable=True, name=item_name) appointment_type = create_appointment_type() @@ -35,6 +42,7 @@ class ViewFunctionsTests(LoggedInTestCase): self.assertTrue(item_name in response.content) def test_kit_requests_4(self): + self.login_as_admin() item_name = "Test item to be ordered" item = Item.objects.create(disposable=True, name=item_name) appointment_type = create_appointment_type() @@ -54,6 +62,7 @@ class ViewFunctionsTests(LoggedInTestCase): self.assertFalse(item_name in response.content) def test_kit_requests_3(self): + self.login_as_admin() item_name = "Test item to be ordered" item = Item.objects.create(disposable=True, name=item_name) appointment_type = create_appointment_type() @@ -72,6 +81,7 @@ class ViewFunctionsTests(LoggedInTestCase): self.assertTrue(item_name in response.content) def test_kit_requests_order(self): + self.login_as_admin() item_name = "Test item to be ordered" item = Item.objects.create(disposable=True, name=item_name) appointment_type = create_appointment_type() @@ -104,6 +114,7 @@ class ViewFunctionsTests(LoggedInTestCase): self.assertEqual(appointment2, result['appointments'][2]) def test_kit_requests_for_appointment_with_two_types(self): + self.login_as_admin() item = Item.objects.create(disposable=True, name="item 1") appointment_type = create_appointment_type() appointment_type.required_equipment.add(item) @@ -129,6 +140,7 @@ class ViewFunctionsTests(LoggedInTestCase): self.assertEqual(1, len(result["appointments"])) def test_kit_requests_send_email(self): + self.login_as_admin() item_name = "Test item to be ordered" item = Item.objects.create(disposable=True, name=item_name) appointment_type = create_appointment_type() @@ -150,6 +162,7 @@ class ViewFunctionsTests(LoggedInTestCase): self.assertEqual(1, len(mail.outbox)) def test_kit_request_send_mail_with_general_appointment(self): + self.login_as_admin() item_name = "Test item to be ordered" item = Item.objects.create(disposable=True, name=item_name) appointment_type = create_appointment_type() diff --git a/smash/web/tests/view/test_mail.py b/smash/web/tests/view/test_mail.py index 900c84a7bcadf38a37c420308d638eb4f2c74d14..1f11340a796aedc4e9c0eebf7662ddd50d3cce33 100644 --- a/smash/web/tests/view/test_mail.py +++ b/smash/web/tests/view/test_mail.py @@ -4,8 +4,8 @@ from django.urls import reverse from web.models import MailTemplate from web.models.constants import MAIL_TEMPLATE_CONTEXT_VOUCHER -from web.tests.functions import create_voucher, get_resource_path from web.tests import LoggedInTestCase +from web.tests.functions import create_voucher, get_resource_path logger = logging.getLogger(__name__) @@ -20,3 +20,12 @@ class MailTests(LoggedInTestCase): page = reverse('web.views.mail_template_generate_for_vouchers') + "?voucher_id=" + str(voucher.id) response = self.client.get(page) self.assertEqual(response.status_code, 200) + + def test_list_mail_templates(self): + self.login_as_admin() + response = self.client.get(reverse("web.views.mail_templates")) + self.assertEqual(response.status_code, 200) + + def test_list_mail_templates_without_permission(self): + response = self.client.get(reverse("web.views.mail_templates")) + self.assertEqual(response.status_code, 302) diff --git a/smash/web/tests/view/test_rooms.py b/smash/web/tests/view/test_rooms.py index ecf1161677c0a104d9861bd356c2c96416b81bf0..b607e8105b025ac6e5492ff5b53f6ec0e9aba7ea 100644 --- a/smash/web/tests/view/test_rooms.py +++ b/smash/web/tests/view/test_rooms.py @@ -2,15 +2,16 @@ import logging from django.urls import reverse -from web.tests.functions import create_room, create_item -from web.models import Item, Room +from web.models import Room from web.tests import LoggedInTestCase +from web.tests.functions import create_room, create_item logger = logging.getLogger(__name__) class RoomsTests(LoggedInTestCase): def test_rooms_requests(self): + self.login_as_admin() pages = [ 'web.views.equipment_and_rooms.rooms', 'web.views.equipment_and_rooms.rooms_add', @@ -20,7 +21,18 @@ class RoomsTests(LoggedInTestCase): response = self.client.get(reverse(page)) self.assertEqual(response.status_code, 200) + def test_rooms_requests_without_permission(self): + pages = [ + 'web.views.equipment_and_rooms.rooms', + 'web.views.equipment_and_rooms.rooms_add', + ] + + for page in pages: + response = self.client.get(reverse(page)) + self.assertEqual(response.status_code, 302) + def test_rooms_edit_request(self): + self.login_as_admin() room = create_room() page = reverse('web.views.equipment_and_rooms.rooms_edit', kwargs={'room_id': str(room.id)}) @@ -28,6 +40,7 @@ class RoomsTests(LoggedInTestCase): self.assertEqual(response.status_code, 200) def test_rooms_delete_request(self): + self.login_as_admin() room = create_room() page = reverse('web.views.equipment_and_rooms.rooms_delete', kwargs={'room_id': str(room.id)}) @@ -35,6 +48,7 @@ class RoomsTests(LoggedInTestCase): self.assertEqual(response.status_code, 302) def test_rooms_add(self): + self.login_as_admin() page = reverse('web.views.equipment_and_rooms.rooms_add') item = create_item() data = { @@ -53,6 +67,7 @@ class RoomsTests(LoggedInTestCase): self.assertEqual(len(freshly_created), 1) def test_rooms_edit(self): + self.login_as_admin() room = create_room() page = reverse('web.views.equipment_and_rooms.rooms_edit', kwargs={'room_id': str(room.id)}) @@ -72,6 +87,7 @@ class RoomsTests(LoggedInTestCase): self.assertEqual(getattr(freshly_edited, key, ''), data[key]) def test_rooms_delete(self): + self.login_as_admin() room = create_room() page = reverse('web.views.equipment_and_rooms.rooms_delete', kwargs={'room_id': str(room.id)}) diff --git a/smash/web/tests/view/test_statistics.py b/smash/web/tests/view/test_statistics.py index 737371076825bc63c5cbbe510da8a156ae5d17a5..aa7fee20b0dcffbe402185e823e885a598ec0850 100644 --- a/smash/web/tests/view/test_statistics.py +++ b/smash/web/tests/view/test_statistics.py @@ -10,6 +10,7 @@ __author__ = 'Valentin Grouès' class TestStatisticsView(LoggedInTestCase): def test_statistics_request(self): + self.login_as_admin() url = reverse('web.views.statistics') response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -19,3 +20,8 @@ class TestStatisticsView(LoggedInTestCase): response = self.client.get(url, {"month": 10, "year": 2017, "subject_type": -1, "visit": -1}) content = response.content self.assertIn('<option value="10" selected>October', content) + + def test_statistics_request_without_permission(self): + url = reverse('web.views.statistics') + response = self.client.get(url) + self.assertEqual(response.status_code, 302) diff --git a/smash/web/tests/view/test_study.py b/smash/web/tests/view/test_study.py index 6b7f18b6e90f2190f03ea9ef233a390807c668af..40df2f29ea6df924021cb5f854c40e6731562b1d 100644 --- a/smash/web/tests/view/test_study.py +++ b/smash/web/tests/view/test_study.py @@ -10,9 +10,9 @@ from web.tests.functions import get_test_study, format_form_field logger = logging.getLogger(__name__) -class SubjectsViewTests(LoggedInWithWorkerTestCase): +class StudyViewTests(LoggedInWithWorkerTestCase): def setUp(self): - super(SubjectsViewTests, self).setUp() + super(StudyViewTests, self).setUp() self.study = get_test_study() def test_render_study_edit(self): diff --git a/smash/web/tests/view/test_subjects.py b/smash/web/tests/view/test_subjects.py index c1a99e76530f7aa12f79e6edb0edd5b08d00ef4a..71f0d3e773fa9cfba8d2cd6c5c3d5988c9bf6eaf 100644 --- a/smash/web/tests/view/test_subjects.py +++ b/smash/web/tests/view/test_subjects.py @@ -1,6 +1,7 @@ import datetime import logging +from django.contrib.auth.models import Permission from django.core.files.uploadedfile import SimpleUploadedFile from django.urls import reverse @@ -23,6 +24,7 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase): self.study = get_test_study() def test_render_subjects_add(self): + self.worker.roles.all()[0].permissions.add(Permission.objects.get(codename="add_subject")) self.worker.save() response = self.client.get(reverse('web.views.subject_add')) @@ -161,6 +163,8 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase): return form_data def test_subjects_add_2(self): + self.worker.roles.all()[0].permissions.add(Permission.objects.get(codename="add_subject")) + self.worker.save() form_data = self.create_add_form_data_for_study_subject() form_data["study_subject-type"] = SUBJECT_TYPE_CHOICES_CONTROL @@ -176,6 +180,8 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase): " as default location prefix is not defined and subject type is control") def test_subjects_add_with_referral_letter_file(self): + self.worker.roles.all()[0].permissions.add(Permission.objects.get(codename="add_subject")) + self.worker.save() StudyColumns.objects.all().update(referral_letter=True) form_data = self.create_add_form_data_for_study_subject() @@ -213,6 +219,8 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase): form_data["study_subject-last_name"] = "Doe" def test_subjects_add_patient(self): + self.worker.roles.all()[0].permissions.add(Permission.objects.get(codename="add_subject")) + self.worker.save() form_data = self.create_add_form_data_for_study_subject() form_data["study_subject-default_location"] = get_test_location().id @@ -227,6 +235,8 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase): " as default location prefix is not defined and subject type is patient") def test_subjects_add_invalid(self): + self.worker.roles.all()[0].permissions.add(Permission.objects.get(codename="add_subject")) + self.worker.save() 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 @@ -237,6 +247,8 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase): self.assertTrue("Invalid data" in response.content) def test_subjects_add_with_prefixed_location(self): + self.worker.roles.all()[0].permissions.add(Permission.objects.get(codename="add_subject")) + self.worker.save() form_data = self.create_add_form_data_for_study_subject() form_data["study_subject-type"] = SUBJECT_TYPE_CHOICES_CONTROL diff --git a/smash/web/views/__init__.py b/smash/web/views/__init__.py index 04dd18e6f48f979fb473a0626544776129c86777..66c3b6339e4afecdf3332e273a6f03d4e1c43705 100644 --- a/smash/web/views/__init__.py +++ b/smash/web/views/__init__.py @@ -52,7 +52,7 @@ def extend_context(params, request): else: #use full name if available, username otherwise if len(request.user.get_full_name()) > 1: - person = request.user.get_full_name() + person = request.user.get_full_name() else: person = request.user.get_username() role = '<No worker information>' @@ -61,6 +61,7 @@ def extend_context(params, request): final_params.update({ 'permissions' : permissions, 'conf_perms' : permissions & PermissionDecorator.codename_groups['configuration'], + 'equipment_perms' : permissions & PermissionDecorator.codename_groups['equipment'], 'person': person, 'role': role, 'notifications': notifications, diff --git a/smash/web/views/appointment_type.py b/smash/web/views/appointment_type.py index 9aa924d1af11b85b9aa54d6a0594866325898277..2637c26ade7f9f8809b8a7ca031368f29142b579 100644 --- a/smash/web/views/appointment_type.py +++ b/smash/web/views/appointment_type.py @@ -11,7 +11,7 @@ class AppointmentTypeListView(ListView, WrappedView): template_name = 'appointment_types/index.html' context_object_name = "appointment_types" - @PermissionDecorator('change_appointmenttype', 'configuration') + @PermissionDecorator('change_appointmenttype', 'equipment') def dispatch(self, *args, **kwargs): return super(AppointmentTypeListView, self).dispatch(*args, **kwargs) @@ -22,7 +22,7 @@ class AppointmentTypeCreateView(CreateView, WrappedView): success_url = reverse_lazy('web.views.appointment_types') success_message = "Appointment type created" - @PermissionDecorator('change_appointmenttype', 'configuration') + @PermissionDecorator('change_appointmenttype', 'equipment') def dispatch(self, *args, **kwargs): return super(AppointmentTypeCreateView, self).dispatch(*args, **kwargs) @@ -34,7 +34,7 @@ class AppointmentTypeEditView(UpdateView, WrappedView): template_name = "appointment_types/edit.html" context_object_name = "appointment_types" - @PermissionDecorator('change_appointmenttype', 'configuration') + @PermissionDecorator('change_appointmenttype', 'equipment') def dispatch(self, *args, **kwargs): return super(AppointmentTypeEditView, self).dispatch(*args, **kwargs) @@ -47,6 +47,6 @@ class AppointmentTypeDeleteView(DeleteView, WrappedView): messages.success(request, "Appointment Type deleted") return super(AppointmentTypeDeleteView, self).delete(request, *args, **kwargs) - @PermissionDecorator('change_appointmenttype', 'configuration') + @PermissionDecorator('change_appointmenttype', 'equipment') def dispatch(self, *args, **kwargs): return super(AppointmentTypeDeleteView, self).dispatch(*args, **kwargs) \ No newline at end of file diff --git a/smash/web/views/daily_planning.py b/smash/web/views/daily_planning.py index 40ab776c754d5bc3c8b89e8d9e1ec70701669f53..d720816f0672bb455260b5fc3f4a9caa9fc4c2db 100644 --- a/smash/web/views/daily_planning.py +++ b/smash/web/views/daily_planning.py @@ -1,12 +1,15 @@ # coding=utf-8 -import logging from django.views.generic import TemplateView -from . import wrap_response + +from web.decorators import PermissionDecorator from web.models.worker_study_role import STUDY_ROLE_CHOICES +from . import wrap_response + class TemplateDailyPlannerView(TemplateView): + @PermissionDecorator('view_daily_planning', 'daily_planning') def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) context['worker_study_roles'] = STUDY_ROLE_CHOICES - return wrap_response(request, 'daily_planning.html', context) \ No newline at end of file + return wrap_response(request, 'daily_planning.html', context) diff --git a/smash/web/views/equipment.py b/smash/web/views/equipment.py index ff498134cab372d0f026c0316f89295d60b83191..4d006e41d3db59e1d8785f6e0d35ddab089cf3d1 100644 --- a/smash/web/views/equipment.py +++ b/smash/web/views/equipment.py @@ -1,11 +1,13 @@ # coding=utf-8 from django.shortcuts import redirect, get_object_or_404 +from web.decorators import PermissionDecorator from . import wrap_response -from ..models import Item from ..forms.forms import ItemForm +from ..models import Item +@PermissionDecorator('change_item', 'equipment') def equipment(request): equipment_list = Item.objects.order_by('-name') context = { @@ -15,6 +17,7 @@ def equipment(request): return wrap_response(request, "equipment_and_rooms/equipment/index.html", context) +@PermissionDecorator('change_item', 'equipment') def equipment_add(request): if request.method == 'POST': form = ItemForm(request.POST) @@ -27,6 +30,7 @@ def equipment_add(request): return wrap_response(request, 'equipment_and_rooms/equipment/add.html', {'form': form}) +@PermissionDecorator('change_item', 'equipment') def equipment_edit(request, equipment_id): the_item = get_object_or_404(Item, id=equipment_id) if request.method == 'POST': @@ -40,6 +44,7 @@ def equipment_edit(request, equipment_id): return wrap_response(request, 'equipment_and_rooms/equipment/edit.html', {'form': form}) +@PermissionDecorator('change_item', 'equipment') def equipment_delete(request, equipment_id): the_item = get_object_or_404(Item, id=equipment_id) the_item.delete() diff --git a/smash/web/views/export.py b/smash/web/views/export.py index 02fc1a82c1cb8a61341f3f1c0af9469209da0a0c..1cb6271207a31855a0199c62945cd654e336849a 100644 --- a/smash/web/views/export.py +++ b/smash/web/views/export.py @@ -5,10 +5,12 @@ import django_excel as excel from django.http import HttpResponse from notifications import get_today_midnight_date +from web.decorators import PermissionDecorator from . import e500_error, wrap_response from ..models import Subject, StudySubject, Appointment +@PermissionDecorator('export_subjects', 'subject') def export_to_csv(request, data_type="subjects"): # Create the HttpResponse object with the appropriate CSV header. selected_fields = request.GET.get('fields', None) @@ -29,6 +31,7 @@ def export_to_csv(request, data_type="subjects"): return response +@PermissionDecorator('export_subjects', 'subject') def export_to_excel(request, data_type="subjects"): selected_fields = request.GET.get('fields', None) filename = data_type + '-' + get_today_midnight_date().strftime("%Y-%m-%d") + ".xls" @@ -53,26 +56,27 @@ class CustomField: DROP_OUT_FIELD = CustomField({'verbose_name': "DROP OUT", 'name': "custom-drop-out"}) APPOINTMENT_TYPE_FIELD = CustomField({ - 'name': 'appointment_types', - 'verbose_name': 'Appointment Types' - }) + 'name': 'appointment_types', + 'verbose_name': 'Appointment Types' +}) STUDY_SUBJECT_FIELDS = [CustomField({ - 'name': 'nd_number', - 'verbose_name' : 'ND number' - })] + 'name': 'nd_number', + 'verbose_name': 'ND number' +})] SUBJECT_FIELDS = [CustomField({ - 'name': 'last_name', - 'verbose_name': 'Family name' - }), + 'name': 'last_name', + 'verbose_name': 'Family name' +}), CustomField({ 'name': 'first_name', 'verbose_name': 'Name' })] VISIT_FIELDS = [CustomField({ - 'name': 'visit_number', - 'verbose_name': 'Visit' - })] + 'name': 'visit_number', + 'verbose_name': 'Visit' +})] + def filter_fields_from_selected_fields(fields, selected_fields): if selected_fields is None: @@ -80,6 +84,7 @@ def filter_fields_from_selected_fields(fields, selected_fields): selected_fields = set(selected_fields.split(',')) return [field for field in fields if field.name in selected_fields] + def get_default_subject_fields(): subject_fields = [] for field in Subject._meta.fields: @@ -91,12 +96,13 @@ def get_default_subject_fields(): subject_fields.append(DROP_OUT_FIELD) return subject_fields + def get_subjects_as_array(selected_fields=None): result = [] - subject_fields = get_default_subject_fields() + subject_fields = get_default_subject_fields() subject_fields = filter_fields_from_selected_fields(subject_fields, selected_fields) - field_names = [field.verbose_name for field in subject_fields] #faster than loop + field_names = [field.verbose_name for field in subject_fields] # faster than loop result.append(field_names) subjects = StudySubject.objects.order_by('-subject__last_name') @@ -105,6 +111,7 @@ def get_subjects_as_array(selected_fields=None): result.append([unicode(s).replace("\n", ";").replace("\r", ";") for s in row]) return result + def subject_to_row_for_fields(study_subject, subject_fields): row = [] for field in subject_fields: @@ -128,31 +135,33 @@ def subject_to_row_for_fields(study_subject, subject_fields): row.append(cell) return row + def get_appointment_fields(): appointments_fields = [] for field in Appointment._meta.fields: if field.name.upper() != "VISIT" and field.name.upper() != "ID" and \ - field.name.upper() != "WORKER_ASSIGNED" and field.name.upper() != "APPOINTMENT_TYPES" and \ - field.name.upper() != "ROOM" and field.name.upper() != "FLYING_TEAM": + field.name.upper() != "WORKER_ASSIGNED" and field.name.upper() != "APPOINTMENT_TYPES" and \ + field.name.upper() != "ROOM" and field.name.upper() != "FLYING_TEAM": appointments_fields.append(field) all_fields = STUDY_SUBJECT_FIELDS + SUBJECT_FIELDS + VISIT_FIELDS + appointments_fields + [APPOINTMENT_TYPE_FIELD] return all_fields, appointments_fields + def get_appointments_as_array(selected_fields=None): result = [] all_fields, appointments_fields = get_appointment_fields() all_fields = filter_fields_from_selected_fields(all_fields, selected_fields) appointments_fields = filter_fields_from_selected_fields(appointments_fields, selected_fields) - field_names = [field.verbose_name for field in all_fields] #faster than loop + field_names = [field.verbose_name for field in all_fields] # faster than loop result.append(field_names) appointments = Appointment.objects.order_by('-datetime_when') for appointment in appointments: - #add field_names ['ND number', 'Family name', 'Name', 'Visit'] first + # add field_names ['ND number', 'Family name', 'Name', 'Visit'] first row = [] for field in STUDY_SUBJECT_FIELDS: if field.verbose_name in field_names: @@ -175,15 +184,16 @@ def get_appointments_as_array(selected_fields=None): for field in appointments_fields: row.append(getattr(appointment, field.name)) if APPOINTMENT_TYPE_FIELD.verbose_name in field_names: - #avoid last comma in the list of appointment types + # avoid last comma in the list of appointment types type_string = ','.join([appointment_type.code for appointment_type in appointment.appointment_types.all()]) row.append(type_string) result.append([unicode(s).replace("\n", ";").replace("\r", ";") for s in row]) return result +@PermissionDecorator('export_subjects', 'subject') def export(request): return wrap_response(request, 'export/index.html', { 'subject_fields': get_default_subject_fields(), 'appointment_fields': get_appointment_fields()[0] - }) \ No newline at end of file + }) diff --git a/smash/web/views/flying_teams.py b/smash/web/views/flying_teams.py index dc6c4752045cef881778084f6ece845aa259ef2f..b0f5c7b01f0403daf1bea1a4f5c9f69c52b4f03e 100644 --- a/smash/web/views/flying_teams.py +++ b/smash/web/views/flying_teams.py @@ -1,11 +1,13 @@ # coding=utf-8 from django.shortcuts import redirect, get_object_or_404 +from web.decorators import PermissionDecorator from . import wrap_response -from ..models import FlyingTeam from ..forms.forms import FlyingTeamAddForm, FlyingTeamEditForm +from ..models import FlyingTeam +@PermissionDecorator('change_flyingteam', 'equipment') def flying_teams(request): flying_team_list = FlyingTeam.objects.order_by('-place') context = { @@ -16,6 +18,8 @@ def flying_teams(request): "equipment_and_rooms/flying_teams/index.html", context) + +@PermissionDecorator('change_flyingteam', 'equipment') def flying_teams_add(request): if request.method == 'POST': form = FlyingTeamAddForm(request.POST) @@ -28,6 +32,7 @@ def flying_teams_add(request): return wrap_response(request, 'equipment_and_rooms/flying_teams/add.html', {'form': form}) +@PermissionDecorator('change_flyingteam', 'equipment') def flying_teams_edit(request, flying_team_id): the_flying_team = get_object_or_404(FlyingTeam, id=flying_team_id) if request.method == 'POST': diff --git a/smash/web/views/kit.py b/smash/web/views/kit.py index 79c42ef5fb89430026ceaa05c20e766f7175e7ca..c099d903f2716b474cadbcc60be70ce2f1d376db 100644 --- a/smash/web/views/kit.py +++ b/smash/web/views/kit.py @@ -13,6 +13,7 @@ from django_cron import CronJobBase, Schedule from django_cron.models import CronJobLog from notifications import get_filter_locations, get_today_midnight_date +from web.decorators import PermissionDecorator from web.models import ConfigurationItem, Language, Worker from web.models.constants import KIT_EMAIL_HOUR_CONFIGURATION_TYPE, \ KIT_EMAIL_DAY_OF_WEEK_CONFIGURATION_TYPE, CRON_JOB_TIMEOUT @@ -60,6 +61,7 @@ def get_kit_requests(user, start_date=None, end_date=None): return result +@PermissionDecorator('send_sample_mail_for_appointments', 'equipment') def get_kit_requests_data(request, start_date=None, end_date=None): form = KitRequestForm() if request.method == 'POST': @@ -76,6 +78,7 @@ def get_kit_requests_data(request, start_date=None, end_date=None): return params +@PermissionDecorator('send_sample_mail_for_appointments', 'equipment') def kit_requests(request): return wrap_response(request, 'equipment_and_rooms/kit_requests/kit_requests.html', get_kit_requests_data(request)) @@ -85,7 +88,7 @@ def send_mail(data): if data["end_date"] is not None: end_date_str = data["end_date"].strftime('%Y-%m-%d') title = "Samples between " + \ - data["start_date"].strftime('%Y-%m-%d') + " and " + end_date_str + data["start_date"].strftime('%Y-%m-%d') + " and " + end_date_str cell_style = "padding: 8px; line-height: 1.42857143; vertical-align: top; " \ "font-size: 14px; font-family: 'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;" @@ -109,10 +112,10 @@ def send_mail(data): row_style = ' background-color: #f9f9f9;' email_body += "<tr style='" + row_style + "'>" email_body += "<td style='" + cell_style + "'>" + \ - appointment.datetime_when.strftime('%Y-%m-%d %H:%M') + "</td>" + appointment.datetime_when.strftime('%Y-%m-%d %H:%M') + "</td>" if appointment.visit is not None and appointment.visit.subject is not None: email_body += "<td style='" + cell_style + "'>" + \ - appointment.visit.subject.nd_number + "</td>" + appointment.visit.subject.nd_number + "</td>" else: email_body += "<td style='" + cell_style + "'>" + '-' + "</td>" email_body += "<td style='" + cell_style + "'>" @@ -126,7 +129,7 @@ def send_mail(data): location += " (" + unicode(appointment.flying_team) + ")" email_body += "<td style='" + cell_style + "'>" + location + "</td>" email_body += "<td style='" + cell_style + "'>" + \ - unicode(appointment.worker_assigned) + "</td>" + unicode(appointment.worker_assigned) + "</td>" email_body += "</tr>" email_body += "</tbody></table>" recipients = ConfigurationItem.objects.get( @@ -136,6 +139,7 @@ def send_mail(data): EmailSender().send_email(title, email_body, recipients, cc_recipients) +@PermissionDecorator('send_sample_mail_for_appointments', 'equipment') def kit_requests_send_mail(request, start_date, end_date=None): data = get_kit_requests_data(request, start_date, end_date) try: diff --git a/smash/web/views/mails.py b/smash/web/views/mails.py index ef4cf0b3bb6ce05c8ff4f35b0246c0f830ea5171..7b49409c99a1f1fafb6c4600bf16fbd9af6897d2 100644 --- a/smash/web/views/mails.py +++ b/smash/web/views/mails.py @@ -9,6 +9,7 @@ from django.urls import reverse_lazy from django.views.generic import DeleteView from django.views.generic import ListView +from web.decorators import PermissionDecorator from web.docx_helper import merge_files from . import WrappedView from . import wrap_response @@ -32,7 +33,11 @@ class MailTemplatesListView(ListView, WrappedView): context_object_name = "mail_templates" template_name = 'mail_templates/list.html' - def get_context_data(self, **kwargs): + @PermissionDecorator('change_mailtemplate', 'mailtemplate') + def dispatch(self, *args, **kwargs): + return super(MailTemplatesListView, self).dispatch(*args, **kwargs) + + def get_context_data(self, *args, **kwargs): context = super(MailTemplatesListView, self).get_context_data() context['explanations'] = {"generic": MailTemplate.MAILS_TEMPLATE_GENERIC_TAGS, "subject": MailTemplate.MAILS_TEMPLATE_SUBJECT_TAGS, @@ -43,6 +48,7 @@ class MailTemplatesListView(ListView, WrappedView): return context +@PermissionDecorator('change_mailtemplate', 'mailtemplate') def mail_template_add(request): if request.method == 'POST': form = MailTemplateForm(request.POST, request.FILES) @@ -59,6 +65,7 @@ def mail_template_add(request): return wrap_response(request, 'mail_templates/add.html', {'form': form}) +@PermissionDecorator('change_mailtemplate', 'mailtemplate') def mail_template_edit(request, pk): template = get_object_or_404(MailTemplate, pk=pk) if request.method == 'POST': @@ -82,6 +89,7 @@ class MailTemplatesDeleteView(DeleteView, WrappedView): success_url = reverse_lazy('web.views.mail_templates') template_name = 'mail_templates/confirm_delete.html' + @PermissionDecorator('change_mailtemplate', 'mailtemplate') def delete(self, request, *args, **kwargs): messages.success(request, "Template deleted") try: diff --git a/smash/web/views/rooms.py b/smash/web/views/rooms.py index 2ff626e95a9259a0769bcb7b8e840a40908952bc..a5ca91933a09431edc200fb0febb51cd43fe3134 100644 --- a/smash/web/views/rooms.py +++ b/smash/web/views/rooms.py @@ -1,11 +1,13 @@ # coding=utf-8 from django.shortcuts import redirect, get_object_or_404 +from web.decorators import PermissionDecorator from . import wrap_response from ..forms.forms import RoomForm from ..models import Room +@PermissionDecorator('change_room', 'equipment') def rooms(request): rooms_list = Room.objects.order_by('-city') context = { @@ -17,6 +19,7 @@ def rooms(request): context) +@PermissionDecorator('change_room', 'equipment') def rooms_add(request): if request.method == 'POST': form = RoomForm(request.POST) @@ -29,6 +32,7 @@ def rooms_add(request): return wrap_response(request, 'equipment_and_rooms/rooms/add.html', {'form': form}) +@PermissionDecorator('change_room', 'equipment') def rooms_edit(request, room_id): the_room = get_object_or_404(Room, id=room_id) if request.method == 'POST': @@ -42,6 +46,7 @@ def rooms_edit(request, room_id): return wrap_response(request, 'equipment_and_rooms/rooms/edit.html', {'form': form}) +@PermissionDecorator('change_room', 'equipment') def rooms_delete(request, room_id): the_room = get_object_or_404(Room, id=room_id) the_room.delete() diff --git a/smash/web/views/statistics.py b/smash/web/views/statistics.py index 67bdb79c8784b427bba9526b546661f266eaf10f..b8cd84cc9cb8316a434b6c8dfe438a038a00f126 100644 --- a/smash/web/views/statistics.py +++ b/smash/web/views/statistics.py @@ -1,9 +1,11 @@ # coding=utf-8 +from web.decorators import PermissionDecorator from . import wrap_response from ..forms import StatisticsForm from ..statistics import StatisticsManager, get_previous_year_and_month +@PermissionDecorator('view_statistics', 'appointment') def statistics(request): statistics_manager = StatisticsManager() visit_choices = [("-1", "all")] diff --git a/smash/web/views/subject.py b/smash/web/views/subject.py index 668346607186f1f5eca9a3e4d9547acc3381a183..ef65d5f486e0aa018a762fa8deb0091a09fdf72a 100644 --- a/smash/web/views/subject.py +++ b/smash/web/views/subject.py @@ -4,6 +4,7 @@ import logging from django.contrib import messages from django.shortcuts import redirect, get_object_or_404 +from web.decorators import PermissionDecorator from . import wrap_response from ..forms import VisitDetailForm, SubjectAddForm, SubjectEditForm, StudySubjectAddForm, StudySubjectEditForm from ..models import StudySubject, MailTemplate, Worker, Study, Provenance, Subject @@ -27,6 +28,7 @@ def subjects(request): return subject_list(request, SUBJECT_LIST_GENERIC) +@PermissionDecorator('add_subject', 'subject') def subject_add(request): study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] if request.method == 'POST': @@ -100,7 +102,7 @@ def subject_edit(request, id): p = Provenance(modified_table = Subject._meta.db_table, modified_table_id = study_subject.subject.id, modification_author = worker, - previous_value = study_subject.subject.dead, + previous_value = was_dead, new_value = True, modification_description = 'Worker "{}" marks subject "{}" as dead'.format(worker, study_subject.subject), modified_field = 'dead', @@ -112,7 +114,7 @@ def subject_edit(request, id): p = Provenance(modified_table = StudySubject._meta.db_table, modified_table_id = study_subject.id, modification_author = worker, - previous_value = study_subject.resigned, + previous_value = was_resigned, new_value = True, modification_description = 'Worker "{}" marks study subject "{}" as resigned from study "{}"'.format(worker, study_subject.nd_number, study_subject.study), modified_field = 'resigned',