diff --git a/requirements.txt b/requirements.txt index 49a543a076f30f5d39b61889939d1ecfedebdedc..03297a8dc9e83fa4bd3869ed53f19306a9589418 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ numpy==1.19.2 pandas==1.1.3 django-datatables-view==1.19.1 phonenumberslite==8.9.14 -Pillow==3.4.2 +Pillow==8.0.1 psycopg2==2.8.6 pycparser==2.19 pyexcel==0.6.4 @@ -60,3 +60,4 @@ urllib3==1.23 whitenoise==5.2.0 xlrd==1.1.0 xlwt==1.3.0 +parameterized==0.7.4 \ No newline at end of file diff --git a/smash/smash/settings.py b/smash/smash/settings.py index 0b20f9916893b6e782d9aed6fe15fe898a3ae558..e147adf43a7ce463b52595802204d800ad3094ea 100644 --- a/smash/smash/settings.py +++ b/smash/smash/settings.py @@ -49,6 +49,7 @@ INSTALLED_APPS = [ ] MIDDLEWARE = [ + 'web.middleware.PrivacyNoticeMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', diff --git a/smash/web/api_urls.py b/smash/web/api_urls.py index 11a02b7b4c553ecb41c5a1272502ab437bf93e3d..a3baf7356589b4d8dc08a3a592fcad90467d458d 100644 --- a/smash/web/api_urls.py +++ b/smash/web/api_urls.py @@ -64,6 +64,7 @@ urlpatterns = [ url(r'^workers/add_extra_availability/(?P<worker_id>\d+)/(?P<start_str_date>\d{4}-\d{2}-\d{2}-\d{2}-\d{2})/(?P<end_str_date>\d{4}-\d{2}-\d{2}-\d{2}-\d{2})/$', worker.add_worker_extra_availability, name='web.api.workers.add_extra_availability'), + url(r'^worker/accept_privacy_notice/$', worker.accept_privacy_notice, name='web.api.workers.accept_privacy_notice'), # daily planning data url(r'^daily_planning/workers/$', worker.workers_for_daily_planning, name='web.api.workers.daily_planning'), diff --git a/smash/web/api_views/worker.py b/smash/web/api_views/worker.py index c956782d4377bb8d365a58dd892aaefcdf429ac0..2ecf1a00a7bb9090abb12a7e34c058bef1049802 100644 --- a/smash/web/api_views/worker.py +++ b/smash/web/api_views/worker.py @@ -26,6 +26,15 @@ def units(request): "units": [x[0] for x in workers] }) + +def accept_privacy_notice(request): + worker = Worker.get_by_user(request.user) + worker.privacy_notice_accepted = True + worker.save() + return JsonResponse({ + "status": 'ok' + }) + def workers_for_daily_planning(request): start_date = request.GET.get('start_date') workers = get_workers_for_daily_planning(request) diff --git a/smash/web/forms/__init__.py b/smash/web/forms/__init__.py index 698f27b4f65ea900c87815a3ffa6fb3b6f08697d..fe81e129a4d942bdb713f4e19f24ccf4a47d9191 100644 --- a/smash/web/forms/__init__.py +++ b/smash/web/forms/__init__.py @@ -1,5 +1,5 @@ from .study_forms import StudyEditForm, StudyNotificationParametersEditForm, StudyColumnsEditForm, StudyRedCapColumnsEditForm -from .worker_form import WorkerForm +from .worker_form import WorkerForm, WorkerAcceptPrivacyNoticeForm from .forms import VisitDetailForm, \ VisitAddForm, KitRequestForm, StatisticsForm, AvailabilityAddForm, \ AvailabilityEditForm, HolidayAddForm @@ -8,10 +8,11 @@ from .appointment_form import AppointmentDetailForm, AppointmentEditForm, Appoin from .study_subject_forms import StudySubjectAddForm, StudySubjectDetailForm, StudySubjectEditForm from .subject_forms import SubjectAddForm, SubjectEditForm, SubjectDetailForm from .voucher_forms import VoucherTypeForm, VoucherTypePriceForm, VoucherForm +from .privacy_notice import PrivacyNoticeForm __all__ = [StudySubjectAddForm, StudySubjectDetailForm, StudySubjectEditForm, WorkerForm, AppointmentDetailForm, AppointmentEditForm, AppointmentAddForm, VisitDetailForm, VisitAddForm, ContactAttemptAddForm, ContactAttemptEditForm, KitRequestForm, StatisticsForm, AvailabilityAddForm, AvailabilityEditForm, HolidayAddForm, SubjectAddForm, SubjectEditForm, SubjectDetailForm, VoucherTypeForm, VoucherTypePriceForm, VoucherForm, StudyEditForm, StudyNotificationParametersEditForm, StudyColumnsEditForm, - StudyRedCapColumnsEditForm] + StudyRedCapColumnsEditForm, PrivacyNoticeForm, WorkerAcceptPrivacyNoticeForm] diff --git a/smash/web/forms/privacy_notice.py b/smash/web/forms/privacy_notice.py new file mode 100644 index 0000000000000000000000000000000000000000..9394fe3687c142c54dbab9ef6d2b0e3c23ccf50e --- /dev/null +++ b/smash/web/forms/privacy_notice.py @@ -0,0 +1,15 @@ +from django import forms +from django.forms import ModelForm +from web.models import PrivacyNotice + +import logging +logger = logging.getLogger(__name__) + +class PrivacyNoticeForm(ModelForm): + class Meta: + model = PrivacyNotice + fields = '__all__' + + def clean(self): + cleaned_data = super().clean() + return cleaned_data \ No newline at end of file diff --git a/smash/web/forms/worker_form.py b/smash/web/forms/worker_form.py index 25c4cc8df80a06ff60c6da7db42bd03dc45d6ec4..f82362938e92ed58bce1c113ec206699a5de5a7a 100644 --- a/smash/web/forms/worker_form.py +++ b/smash/web/forms/worker_form.py @@ -14,6 +14,19 @@ from web.decorators import PermissionDecorator logger = logging.getLogger(__name__) +class WorkerAcceptPrivacyNoticeForm(ModelForm): + class Meta: + model = Worker + fields = ('privacy_notice_accepted', ) + + def __init__(self, *args, **kwargs): + super(WorkerAcceptPrivacyNoticeForm, self).__init__(*args, **kwargs) + self.fields['privacy_notice_accepted'].label = 'Do you accept the privacy notice?' + + def clean(self): + cleaned_data = super().clean() + cleaned_data['privacy_notice_accepted'] = True + return cleaned_data class WorkerForm(ModelForm): class Meta: @@ -63,7 +76,9 @@ class WorkerForm(ModelForm): self.fields['Superuser'].initial = instance.user.is_superuser del self.fields['voucher_types'] del self.fields['name'] + self.fields['privacy_notice_accepted'].widget.attrs['readonly'] = True else: + del self.fields['privacy_notice_accepted'] del self.fields['locations'] del self.fields['first_name'] del self.fields['last_name'] @@ -73,7 +88,7 @@ class WorkerForm(ModelForm): fields = OrderedDict() - if worker_type == WORKER_STAFF: + if worker_type == WORKER_STAFF: if instance is None or instance.pk is None: fields['login'] = forms.CharField(label='Login') fields['password'] = forms.CharField(label='Password', widget=forms.PasswordInput) diff --git a/smash/web/middleware.py b/smash/web/middleware.py new file mode 100644 index 0000000000000000000000000000000000000000..e094a84f09c91c120fb0634d2b2e196c4a6cedea --- /dev/null +++ b/smash/web/middleware.py @@ -0,0 +1,39 @@ +from django.contrib.auth.views import logout + +from web.models.constants import GLOBAL_STUDY_ID +from web.models import Worker, Study +from django.contrib import messages +from django.urls import reverse +from django.shortcuts import redirect +from django.utils.deprecation import MiddlewareMixin +from web.views.privacy_notice import privacy_notice_accept + +class PrivacyNoticeMiddleware(MiddlewareMixin): + #def __init__(self, get_response): + #self.get_response = get_response + # One-time configuration and initialization. + + def process_view(self, request, view_func, view_args, view_kwargs): + # Code to be executed for each request before + # the view (and later middleware) are called. + + #response = self.get_response(request) + if request.user.is_authenticated \ + and not view_func == privacy_notice_accept \ + and not request.user.is_superuser \ + and not view_func == logout: + study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] + worker = Worker.get_by_user(request.user) + if worker is None: + return None + if study.study_privacy_notice \ + and study.acceptance_of_study_privacy_notice_required \ + and not worker.privacy_notice_accepted\ + and study.study_privacy_notice.document.url != request.path: + messages.add_message(request, messages.WARNING, "You can't use the system until you accept the privacy notice.") + #return reverse_lazy('web.views.accept_privacy_notice', kwargs={'pk': study.study_privacy_notice}) + return redirect(reverse('web.views.accept_privacy_notice', kwargs={'pk': study.study_privacy_notice.id})) + + # Code to be executed for each request/response after + # the view is called. + return None \ No newline at end of file diff --git a/smash/web/migrations/0177_auto_20201116_1508.py b/smash/web/migrations/0177_auto_20201116_1508.py new file mode 100644 index 0000000000000000000000000000000000000000..969d1db84635e6c75d2604ae95110eea53085dd4 --- /dev/null +++ b/smash/web/migrations/0177_auto_20201116_1508.py @@ -0,0 +1,29 @@ +# Generated by Django 2.0.13 on 2020-11-16 15:08 + +import django.core.files.storage +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0176_configurationitem_local_setting_clean'), + ] + + operations = [ + migrations.CreateModel( + name='PrivacyNotice', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('document', models.FileField(editable=False, upload_to='privacy_notices/', verbose_name='Study Privacy Notice file')), + ], + ), + migrations.AddField( + model_name='study', + name='study_privacy_notice', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='studies', to='web.PrivacyNotice', verbose_name='Study Privacy Note'), + ) + ] diff --git a/smash/web/migrations/0177_auto_20201116_1508_squashed_0180_auto_20201117_0838.py b/smash/web/migrations/0177_auto_20201116_1508_squashed_0180_auto_20201117_0838.py new file mode 100644 index 0000000000000000000000000000000000000000..0ea6aaa3e07eff90c830ec708ad47c730bdaf199 --- /dev/null +++ b/smash/web/migrations/0177_auto_20201116_1508_squashed_0180_auto_20201117_0838.py @@ -0,0 +1,31 @@ +# Generated by Django 2.0.13 on 2020-11-17 08:48 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + replaces = [('web', '0177_auto_20201116_1508'), ('web', '0178_auto_20201116_1513'), ('web', '0179_auto_20201116_1528'), ('web', '0180_auto_20201117_0838')] + + dependencies = [ + ('web', '0176_configurationitem_local_setting_clean'), + ] + + operations = [ + migrations.CreateModel( + name='PrivacyNotice', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('document', models.FileField(editable=False, upload_to='privacy_notices/', verbose_name='Study Privacy Notice file')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(default='', max_length=255, verbose_name='Name')), + ], + ), + migrations.AddField( + model_name='study', + name='study_privacy_notice', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='studies', to='web.PrivacyNotice', verbose_name='Study Privacy Note'), + ), + ] diff --git a/smash/web/migrations/0178_auto_20201116_1513.py b/smash/web/migrations/0178_auto_20201116_1513.py new file mode 100644 index 0000000000000000000000000000000000000000..c3c4c4cc683cd3adc24cb7dbcbdf5d9421d8e3d5 --- /dev/null +++ b/smash/web/migrations/0178_auto_20201116_1513.py @@ -0,0 +1,21 @@ +# Generated by Django 2.0.13 on 2020-11-16 15:13 + +import django.core.files.storage +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0177_auto_20201116_1508'), + ] + + operations = [ + migrations.AddField( + model_name='privacynotice', + name='study', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='web.Study', verbose_name='Study'), + ) + ] diff --git a/smash/web/migrations/0179_auto_20201116_1528.py b/smash/web/migrations/0179_auto_20201116_1528.py new file mode 100644 index 0000000000000000000000000000000000000000..da1ab045b22d246df336c1123b8be7f21ade2382 --- /dev/null +++ b/smash/web/migrations/0179_auto_20201116_1528.py @@ -0,0 +1,21 @@ +# Generated by Django 2.0.13 on 2020-11-16 15:28 + +import django.core.files.storage +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0178_auto_20201116_1513'), + ] + + operations = [ + migrations.AddField( + model_name='privacynotice', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ) + ] diff --git a/smash/web/migrations/0179_merge_20201117_1048.py b/smash/web/migrations/0179_merge_20201117_1048.py new file mode 100644 index 0000000000000000000000000000000000000000..036333dbf183e05258b8d8fd47f8e7b026452c96 --- /dev/null +++ b/smash/web/migrations/0179_merge_20201117_1048.py @@ -0,0 +1,14 @@ +# Generated by Django 2.0.13 on 2020-11-17 10:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0178_auto_20201116_1250'), + ('web', '0177_auto_20201116_1508_squashed_0180_auto_20201117_0838'), + ] + + operations = [ + ] diff --git a/smash/web/migrations/0180_auto_20201117_0838.py b/smash/web/migrations/0180_auto_20201117_0838.py new file mode 100644 index 0000000000000000000000000000000000000000..da010905fdfbc27df5f524ddf6c94a6f1106f91c --- /dev/null +++ b/smash/web/migrations/0180_auto_20201117_0838.py @@ -0,0 +1,31 @@ +# Generated by Django 2.0.13 on 2020-11-17 08:38 + +import django.core.files.storage +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0179_auto_20201116_1528'), + ] + + operations = [ + migrations.RemoveField( + model_name='privacynotice', + name='study', + ), + migrations.AddField( + model_name='privacynotice', + name='name', + field=models.CharField(default='', max_length=255, verbose_name='Name'), + preserve_default=False, + ), + migrations.AlterField( + model_name='study', + name='study_privacy_notice', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='studies', to='web.PrivacyNotice', verbose_name='Study Privacy Note'), + ) + ] diff --git a/smash/web/migrations/0180_auto_20201120_1554.py b/smash/web/migrations/0180_auto_20201120_1554.py new file mode 100644 index 0000000000000000000000000000000000000000..12474a8aebcbd8176fa2562489f2d82d17742dc9 --- /dev/null +++ b/smash/web/migrations/0180_auto_20201120_1554.py @@ -0,0 +1,50 @@ +# Generated by Django 2.0.13 on 2020-11-20 15:54 + +import django.core.files.storage +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0179_merge_20201117_1048'), + ] + + operations = [ + migrations.AddField( + model_name='privacynotice', + name='summary', + field=models.CharField(default='description', max_length=255, verbose_name='Summary'), + preserve_default=False, + ), + migrations.AddField( + model_name='study', + name='acceptance_of_study_privacy_notice_required', + field=models.BooleanField(default=False, verbose_name='Is privacy notice acceptance required?'), + ), + migrations.AlterField( + model_name='privacynotice', + name='created_at', + field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), + ), + migrations.AlterField( + model_name='privacynotice', + name='document', + field=models.FileField(upload_to='privacy_notices/', verbose_name='Study Privacy Notice file'), + ), + migrations.AlterField( + model_name='privacynotice', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='privacynotice', + name='updated_at', + field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), + ), + migrations.AlterField( + model_name='studysubject', + name='referral_letter', + field=models.FileField(blank=True, null=True, storage=django.core.files.storage.FileSystemStorage(location='/tmp/upload'), upload_to='referral_letters', verbose_name='Referral letter'), + ), + ] diff --git a/smash/web/migrations/0181_worker_privacy_notice_accepted.py b/smash/web/migrations/0181_worker_privacy_notice_accepted.py new file mode 100644 index 0000000000000000000000000000000000000000..3677558e321d5989c69eaf39163851c6e6fddc47 --- /dev/null +++ b/smash/web/migrations/0181_worker_privacy_notice_accepted.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.13 on 2020-11-20 15:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0180_auto_20201120_1554'), + ] + + operations = [ + migrations.AddField( + model_name='worker', + name='privacy_notice_accepted', + field=models.BooleanField(default=False, verbose_name='Has accepted privacy notice?'), + ), + ] diff --git a/smash/web/models/__init__.py b/smash/web/models/__init__.py index 9c193eaa042b0662683c6f399f21b5053222e89d..46f20636c434b587773fe96e454b017f2248cb14 100644 --- a/smash/web/models/__init__.py +++ b/smash/web/models/__init__.py @@ -38,6 +38,7 @@ from .contact_attempt import ContactAttempt from .mail_template import MailTemplate from .missing_subject import MissingSubject from .inconsistent_subject import InconsistentSubject, InconsistentField +from .privacy_notice import PrivacyNotice __all__ = [Study, FlyingTeam, Appointment, AppointmentType, Availability, Holiday, Item, Language, Location, Room, Subject, StudySubject, StudySubjectList, SubjectColumns, StudyNotificationParameters, diff --git a/smash/web/models/privacy_notice.py b/smash/web/models/privacy_notice.py new file mode 100644 index 0000000000000000000000000000000000000000..12c0696145f12c5b569a0595993c69e290532a98 --- /dev/null +++ b/smash/web/models/privacy_notice.py @@ -0,0 +1,21 @@ +# coding=utf-8 +import datetime + +from django.db import models +from web.templatetags.filters import basename + +class PrivacyNotice(models.Model): + name = models.CharField(max_length=255, verbose_name='Name') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='Created at') + updated_at = models.DateTimeField(auto_now=True, verbose_name='Updated at') + summary = models.CharField(max_length=255, verbose_name='Summary', blank=False, null=False) + document = models.FileField(upload_to='privacy_notices/', + verbose_name='Study Privacy Notice file', + null=False, editable=True) + + def __str__(self): + return f'{self.name} ({basename(self.document.url)})' + + @property + def all_studies(self): + return self.studies.all() diff --git a/smash/web/models/study.py b/smash/web/models/study.py index 41006b1e335c42c217a35b925ed5b2db454ae741..83f438ebaa452e7a5ca9ae442b5ccb45b8d65b3e 100644 --- a/smash/web/models/study.py +++ b/smash/web/models/study.py @@ -94,6 +94,18 @@ class Study(models.Model): blank=False ) + study_privacy_notice = models.ForeignKey("web.PrivacyNotice", + verbose_name='Study Privacy Note', + editable=True, blank=True, + null=True, on_delete=models.SET_NULL, + related_name='studies' + ) + + acceptance_of_study_privacy_notice_required = models.BooleanField( + default=False, + verbose_name="Is privacy notice acceptance required?" + ) + def check_nd_number(self, nd_number): regex = re.compile(self.nd_number_study_subject_regex) return regex.match(nd_number) is not None diff --git a/smash/web/models/worker.py b/smash/web/models/worker.py index 83793ffd80514f25d2df2950b0caa38a6ce95caa..8de5a4258bacc945bcd73fb346c9b5cab0e8ed8f 100644 --- a/smash/web/models/worker.py +++ b/smash/web/models/worker.py @@ -154,6 +154,10 @@ class Worker(models.Model): blank=True ) + privacy_notice_accepted = models.BooleanField( + default=False, + verbose_name="Has accepted privacy notice?") + def is_on_leave(self): if len(self.holiday_set.filter(datetime_end__gt=timezone.now(), datetime_start__lt=timezone.now(), diff --git a/smash/web/templates/_base.html b/smash/web/templates/_base.html index b5ec2ee0002e84a2b0fdf164914f2742f5e1e8ea..688db99e33a405591cbaeaad32c84fca14fbd5bf 100644 --- a/smash/web/templates/_base.html +++ b/smash/web/templates/_base.html @@ -32,6 +32,8 @@ <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> <![endif]--> + <script src="https://cdn.jsdelivr.net/npm/cookieconsent@3/build/cookieconsent.min.js"></script> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cookieconsent@3/build/cookieconsent.min.css"> {% endblock styles %} </head> <!-- @@ -73,7 +75,7 @@ desired effect <a href="#" class="sidebar-toggle" data-toggle="offcanvas" role="button"> <span class="sr-only">Toggle navigation</span> </a> - + <div class="col-xs-7 navbar-text" style="margin-left: 0px"> <div class="warning_ticker"> <div class="ticker_list"> @@ -388,7 +390,7 @@ desired effect mousePause: 1 }); }else if($('.ticker_list').children().length == 1){ - + }else{ $('.warning_ticker').css('display', 'none'); } @@ -397,12 +399,42 @@ desired effect var $e = $(".sidebar-menu li[data-desc='" + page_to_activate + "']"); $e.addClass("active"); if($($e).parents('li[data-desc]').length > 0){ //if there is a parent, it should also be active - $($e).parents('li[data-desc]').addClass("active"); + $($e).parents('li[data-desc]').addClass("active"); } - + }; activate({% block ui_active_tab %}{% endblock ui_active_tab %}); + if ("{{ show_notice }}".toLowerCase() === "true") { + window.cookieconsent.initialise({ + container: document.getElementById("content"), + content: { + header: 'Privacy notice!', + message: '{{ study.study_privacy_notice.summary}}', + href: '{{ study.study_privacy_notice.document.url }}', + dismiss: 'Got it!', + }, + palette: { + popup: {background: "#fff"}, + button: {background: "#aa0000"}, + }, + revokable: true, + onStatusChange: function (status) { + if (this.hasConsented()) { + $.ajax({ + url: "{% url 'web.api.workers.accept_privacy_notice' %}", + success: function (result) { + if (result.isOk === false) console.log(result.message); + }, + async: false + }); + } + }, + law: { + regionalLaw: false, + } + }); + } </script> {% comment "TODO: Check, and add if works %} diff --git a/smash/web/templates/privacy_notice/acceptance_study_privacy_notice.html b/smash/web/templates/privacy_notice/acceptance_study_privacy_notice.html new file mode 100644 index 0000000000000000000000000000000000000000..e2c1a1d6f58e1c446e3727aff344427f72b53773 --- /dev/null +++ b/smash/web/templates/privacy_notice/acceptance_study_privacy_notice.html @@ -0,0 +1,62 @@ +{% extends "_base.html" %} +{% load static %} +{% load filters %} + +{% block styles %} + {{ block.super }} +{% endblock styles %} + +{% block ui_active_tab %}'workers'{% endblock ui_active_tab %} +{% block page_description %}{% endblock page_description %} + +{% block maincontent %} + + {% block content %} + <div class="row"> + <div class="col-md-12"> + <div class="box box-success"> + <div class="box-header with-border"> + <h3 class="box-title">{% block form-title %}Check the privacy notice{% endblock %}</h3> + </div> + + + <form method="post" action="" class="form-horizontal" enctype="multipart/form-data"> + {% csrf_token %} + + <div class="box-body"> + <div class="form-group {% if field.errors %}has-error{% endif %}"> + <label class="col-sm-4 col-lg-offset-1 col-lg-2 control-label"> + <p >{{privacy_notice.summary}}. Read <a href="{{ privacy_notice.document.url }}">more</a></p> + </label> + </div> + </div><!-- /.box-body --> + <div class="box-footer"> + <div class="col-sm-6"> + <a class="btn btn-block btn-info" href="{% url 'logout' %}"> + I do not agree + </a> + </div> + <div class="col-sm-6"> + <button type="submit" class="btn btn-block btn-success">{% block save-button %} + I agree{% endblock %}</button> + </div> + </div><!-- /.box-footer --> + </form> + </div> + + </div> + </div> + + {% endblock %} + + +{% endblock maincontent %} + +{% block scripts %} + {{ block.super }} + + <script type="text/javascript"> + + + </script> +{% endblock scripts %} \ No newline at end of file diff --git a/smash/web/templates/privacy_notice/add.html b/smash/web/templates/privacy_notice/add.html new file mode 100644 index 0000000000000000000000000000000000000000..6d4f87bf936db78ad7926b81338120374ef97eca --- /dev/null +++ b/smash/web/templates/privacy_notice/add.html @@ -0,0 +1,8 @@ +{% extends "privacy_notice/add_edit.html" %} + +{% block page_header %}New privacy notice{% endblock page_header %} + +{% block title %}{{ block.super }} - Add new privacy notice{% endblock %} + +{% block form-title %}Enter privacy notice details{% endblock %} +{% block save-button %}Add{% endblock %} \ No newline at end of file diff --git a/smash/web/templates/privacy_notice/add_edit.html b/smash/web/templates/privacy_notice/add_edit.html new file mode 100644 index 0000000000000000000000000000000000000000..17ad93d68448db002bc8e4c813749e38dacabfce --- /dev/null +++ b/smash/web/templates/privacy_notice/add_edit.html @@ -0,0 +1,83 @@ +{% extends "_base.html" %} +{% load static %} +{% load filters %} + +{% block styles %} + {{ block.super }} + <link rel="stylesheet" href="{% static 'AdminLTE/plugins/awesomplete/awesomplete.css' %}"/> + <style type="text/css"> + .help_text{ + margin-left: 5px; + } + </style> +{% endblock styles %} + +{% block ui_active_tab %}'subjects'{% endblock ui_active_tab %} +{% block page_description %}{% endblock page_description %} + +{% block breadcrumb %} + {% include "privacy_notice/breadcrumb.html" %} +{% endblock breadcrumb %} + +{% block maincontent %} + + {% block content %} + <div class="row"> + <div class="col-md-12"> + <div class="box box-success"> + <div class="box-header with-border"> + <h3 class="box-title">{% block form-title %}Enter privacy notice details{% endblock %}</h3> + </div> + + + <form method="post" action="" class="form-horizontal" enctype="multipart/form-data"> + {% csrf_token %} + + <div class="box-body"> + {% for field in form %} + <div class="form-group {% if field.errors %}has-error{% endif %}"> + <label class="col-sm-4 col-lg-offset-1 col-lg-2 control-label"> + {{ field.label }} + {% if field.help_text %} + <i class="fa fa-info-circle help_text" aria-hidden="true" data-toggle="tooltip" data-placement="top" title="{{field.help_text}}"></i> + {% endif %} + </label> + + <div class="col-sm-8 col-lg-4"> + {{ field|add_class:'form-control' }} + {% if field.errors %} + <span class="help-block">{{ field.errors }}</span> + {% endif %} + </div> + + + </div> + {% endfor %} + </div><!-- /.box-body --> + <div class="box-footer"> + <div class="col-sm-6"> + <button type="submit" class="btn btn-block btn-success">{% block save-button %} + Add{% endblock %}</button> + </div> + <div class="col-sm-6"> + <a href="{% url 'web.views.privacy_notices' %}" + class="btn btn-block btn-default">Cancel</a> + </div> + </div><!-- /.box-footer --> + </form> + </div> + + </div> + </div> + + {% endblock %} + + +{% endblock maincontent %} + +{% block scripts %} + {{ block.super }} + + <script src="{% static 'AdminLTE/plugins/awesomplete/awesomplete.min.js' %}"></script> + +{% endblock scripts %} \ No newline at end of file diff --git a/smash/web/templates/privacy_notice/breadcrumb.html b/smash/web/templates/privacy_notice/breadcrumb.html new file mode 100644 index 0000000000000000000000000000000000000000..3a3bdd23e7829eec33bf65d789fd90b566940f2b --- /dev/null +++ b/smash/web/templates/privacy_notice/breadcrumb.html @@ -0,0 +1,2 @@ +<li><a href="{% url 'web.views.appointments' %}"><i class="fa fa-dashboard"></i> Dashboard</a></li> +<li class="active"><a href="{% url 'web.views.privacy_notices' %}">Privacy Notices</a></li> \ No newline at end of file diff --git a/smash/web/templates/privacy_notice/confirm_delete.html b/smash/web/templates/privacy_notice/confirm_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..8e618e11025dea0fff578d6dca8546038dadf85f --- /dev/null +++ b/smash/web/templates/privacy_notice/confirm_delete.html @@ -0,0 +1,62 @@ +{% extends "_base.html" %} +{% load static %} +{% load filters %} + +{% block styles %} + {{ block.super }} + <link rel="stylesheet" href="{% static 'AdminLTE/plugins/awesomplete/awesomplete.css' %}"/> + +{% endblock styles %} + +{% block ui_active_tab %}'subjects'{% endblock ui_active_tab %} +{% block page_header %}Delete privacy notice{% endblock page_header %} +{% block page_description %}{% endblock page_description %} + +{% block title %}{{ block.super }} - Delete privacy notice{% endblock %} + +{% block breadcrumb %} + {% include "privacy_notice/breadcrumb.html" %} +{% endblock breadcrumb %} + +{% block maincontent %} + + {% block content %} + <div class="row"> + <div class="col-md-12"> + <div class="box box-success"> + <div class="box-header with-border"> + <h3 class="box-title">Confirm deletion</h3> + </div> + + <form action="" method="post" class="form-horizontal">{% csrf_token %} + <div class="box-body"> + <p>Are you sure you want to delete privacy notice "{{ object.document.url |Â basename }}" from Study "{{object.study}}"?</p> + </div><!-- /.box-body --> + <div class="box-footer"> + <div class="col-sm-6"> + <button type="submit" class="btn btn-block btn-danger">Delete</button> + </div> + <div class="col-sm-6"> + <a href="{% url 'web.views.privacy_notices' %}" + class="btn btn-block btn-default">Cancel</a> + </div> + </div><!-- /.box-footer --> + </form> + </div> + + </div> + </div> + + {% endblock %} + + +{% endblock maincontent %} + +{% block scripts %} + {{ block.super }} + + <script src="{% static 'AdminLTE/plugins/awesomplete/awesomplete.min.js' %}"></script> + +{% endblock scripts %} + + diff --git a/smash/web/templates/privacy_notice/edit.html b/smash/web/templates/privacy_notice/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..f1f0d651f3d94b4c6f27a7fcea64d0d4f86dcaf1 --- /dev/null +++ b/smash/web/templates/privacy_notice/edit.html @@ -0,0 +1,9 @@ +{% extends "privacy_notice/add_edit.html" %} + +{% block page_header %}Edit privacy notice "{{ privacy_notice.name }}"{% endblock page_header %} + +{% block title %}{{ block.super }} - Edit privacy notice "{{ privacy_notice.name }}"{% endblock %} + +{% block form-title %}Enter privacy notice details{% endblock %} + +{% block save-button %}Save{% endblock %} diff --git a/smash/web/templates/privacy_notice/list.html b/smash/web/templates/privacy_notice/list.html new file mode 100644 index 0000000000000000000000000000000000000000..f9a4ebe47515723a05a3d2082301265e6bbf9121 --- /dev/null +++ b/smash/web/templates/privacy_notice/list.html @@ -0,0 +1,92 @@ +{% extends "_base.html" %} +{% load static %} +{% load filters %} + +{% block styles %} + {{ block.super }} + <!-- DataTables --> + <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}"> +{% endblock styles %} + +{% block ui_active_tab %}'privacy_notices'{% endblock ui_active_tab %} +{% block page_header %}Privacy Notices{% endblock page_header %} +{% block page_description %}{% endblock page_description %} + +{% block breadcrumb %} + {% include "privacy_notice/breadcrumb.html" %} +{% endblock breadcrumb %} + +{% block maincontent %} + + + <div class="box box-success"> + <div class="box-header with-border"> + <h3 class="box-title">Privacy Notices</h3> + </div> + <div class="box-body"> + + <div> + <a class="btn btn-app" href="{% url 'web.views.privacy_notice_add' %}"> + <i class="fa fa-plus"></i> Add new privacy notice + </a> + </div> + <table id="table" class="table table-bordered table-striped"> + <thead> + <tr> + <th>No.</th> + <th>Name</th> + <th>Studies</th> + <th>Creation Date</th> + <th>Last Updated</th> + <th>Download</th> + <th>Edit</th> + <th>Delete</th> + </tr> + </thead> + <tbody> + {% for privacy_notice in privacy_notices %} + <tr> + <td>{{ forloop.counter }}</td> + <td>{{ privacy_notice.name }}</td> + <td> + <ul> + {% for study in privacy_notice.all_studies %} + <li>{{ study }}</li> + {% endfor %} + </ul> + </td> + <td>{{ privacy_notice.created_at }}</td> + <td>{{ privacy_notice.updated_at }}</td> + <td><a href="{{ privacy_notice.document.url }}"><i class="fa fa-download"></i></a> {{privacy_notice.document.url |Â basename}}</td> + <td><a href="{% url 'web.views.privacy_notice_edit' privacy_notice.id %}"><i + class="fa fa-edit"></i></a></td> + <td><a href="{% url 'web.views.privacy_notice_delete' privacy_notice.id %}"><i + class="fa fa-trash text-danger"></i></a></td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + +{% endblock maincontent %} + +{% block scripts %} + {{ block.super }} + + <script src="{% static 'AdminLTE/plugins/datatables/jquery.dataTables.min.js' %}"></script> + <script src="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.min.js' %}"></script> + + <script> + $(function () { + $('#table').DataTable({ + "paging": true, + "lengthChange": false, + "searching": true, + "ordering": true, + "info": true, + "autoWidth": false + }); + }); + </script> +{% endblock scripts %} diff --git a/smash/web/templates/study/edit.html b/smash/web/templates/study/edit.html index 5808f33837a87adc20dda1b7ec7f9ca0d9591a0a..ef795d8879bb28bd13c90457bc743c9c00ca4b0c 100644 --- a/smash/web/templates/study/edit.html +++ b/smash/web/templates/study/edit.html @@ -6,14 +6,19 @@ {{ block.super }} <!-- DataTables --> <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}"> + {% include "includes/datepicker.css.html" %} + {% include "includes/datetimepicker.css.html" %} <style type="text/css"> .tooltip-inner { - max-width: 350px; - width: 350px; + max-width: 200px; + width: 200px; + } + .privacy-notice-buttons { + float: right; + padding: 0; + margin-top: 7px; } </style> - {% include "includes/datepicker.css.html" %} - {% include "includes/datetimepicker.css.html" %} {% endblock styles %} {% block ui_active_tab %}'study_conf'{% endblock ui_active_tab %} @@ -55,10 +60,29 @@ data-placement="bottom" title="{{ field.help_text }}"></i> {% endif %} </label> - - <div class="col-sm-8"> - {{ field|add_class:'form-control' }} - </div> + + {% if field.label == 'Study Privacy Note' %} + <div class="col-sm-7"> + {{ field|add_class:'form-control' }} + </div> + <div class="col-sm-1 privacy-notice-buttons"> + {% if privacy_notice %} + <a title="Edit selected privacy notice" href="{% url 'web.views.privacy_notice_edit' field.value %}"> + <i class="fa fa-edit"></i> + </a> + <a title="Download selected privacy notice" href="{{ privacy_notice.document.url }}"> + <i class="fa fa-download"></i> + </a> + {% endif %} + <a title="Add privacy notice" href="{% url 'web.views.privacy_notice_add' %}"> + <i class="fa fa-plus"></i> + </a> + </div> + {% else %} + <div class="col-sm-8"> + {{ field|add_class:'form-control' }} + </div> + {% endif %} {% if field.errors %} <span class="help-block"> {{ field.errors }} </span> @@ -178,4 +202,10 @@ {% include "includes/datepicker.js.html" %} {% include "includes/datetimepicker.js.html" %} + + <script> + $(function(){ + $('a[title]').tooltip({container: 'body'}) + }) + </script> {% endblock scripts %} diff --git a/smash/web/templatetags/filters.py b/smash/web/templatetags/filters.py index a7a63383cf825187fbac57307b97da48f68a0da0..6e2e122ef8c1ea1bc89c060ff220de717fc720be 100644 --- a/smash/web/templatetags/filters.py +++ b/smash/web/templatetags/filters.py @@ -2,7 +2,7 @@ from django import template from django.forms import CheckboxSelectMultiple, CheckboxInput from django.utils.safestring import mark_safe -import datetime +import datetime, os from web.models import ConfigurationItem from web.models.constants import VISIT_SHOW_VISIT_NUMBER_FROM_ZERO from distutils.util import strtobool @@ -59,4 +59,8 @@ def display_visit_number(visit_number): if strtobool(visit_from_zero): return (visit_number - 1) else: - return visit_number \ No newline at end of file + return visit_number + +@register.filter(name='basename') +def basename(path): + return os.path.basename(path) \ No newline at end of file diff --git a/smash/web/tests/view/test_privacy_notice.py b/smash/web/tests/view/test_privacy_notice.py new file mode 100644 index 0000000000000000000000000000000000000000..609d46b55efc863445b80abf758a195bb08642aa --- /dev/null +++ b/smash/web/tests/view/test_privacy_notice.py @@ -0,0 +1,126 @@ +from web.tests.functions import get_resource_path, get_test_study +from web.models import PrivacyNotice, Study, Worker +from web.forms import PrivacyNoticeForm, WorkerAcceptPrivacyNoticeForm +from web.tests import LoggedInTestCase +from django.urls import reverse +from django.core.files.uploadedfile import SimpleUploadedFile +from django.contrib.messages import get_messages +from web.models.constants import GLOBAL_STUDY_ID + +class PrivacyNoticeTests(LoggedInTestCase): + def test_add_privacy_notice(self): + self.assertEqual(0, PrivacyNotice.objects.count()) + self.login_as_admin() + + form_data = dict( + name='example', + summary='example summary' + ) + + file_data = dict( + document=SimpleUploadedFile('file.txt', b"file_content") + ) + + form = PrivacyNoticeForm(form_data, file_data) + self.assertTrue(form.is_valid()) + + page = reverse('web.views.privacy_notice_add') + response = self.client.post(page, data={**form_data, **file_data}) + self.assertEqual(response.status_code, 302) + self.assertEqual(1, PrivacyNotice.objects.count()) + + def test_edit_privacy_notice(self): + self.test_add_privacy_notice() + self.assertEqual(1, PrivacyNotice.objects.count()) + pn = PrivacyNotice.objects.all()[0] + form_data = dict( + name='example2', + summary=pn.summary + ) + + file_data = dict( + document=SimpleUploadedFile('file.txt', b"file_content") + ) + + form = PrivacyNoticeForm(form_data, file_data, instance=pn) + self.assertTrue(form.is_valid()) + + page = reverse('web.views.privacy_notice_edit', kwargs={'pk': pn.id}) + response = self.client.post(page, data={**form_data, **file_data}) + self.assertEqual(response.status_code, 302) + pn = PrivacyNotice.objects.all()[0] + self.assertEqual(pn.name, 'example2') + + def test_delete_privacy_notice(self): + self.test_add_privacy_notice() + self.assertEqual(1, PrivacyNotice.objects.count()) + pn = PrivacyNotice.objects.all()[0] + page = reverse('web.views.privacy_notice_delete', kwargs={'pk': pn.id}) + response = self.client.post(page) + self.assertEqual(response.status_code, 302) + self.assertEqual(0, PrivacyNotice.objects.count()) + + def test_privacy_notice_middleware_superuser(self): + self.test_add_privacy_notice() + + self.login_as_admin() + #assign privacy notice + pn = PrivacyNotice.objects.all()[0] + study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] + study.acceptance_of_study_privacy_notice_required = True + study.study_privacy_notice = pn + study.save() + + study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] + self.assertEqual(study.acceptance_of_study_privacy_notice_required, True) + self.assertEqual(study.study_privacy_notice.id, pn.id) + + self.login_as_super() + self.assertEqual(self.staff_worker.privacy_notice_accepted, False) + page = reverse('web.views.appointments') + response = self.client.get(page) + self.assertEqual(response.status_code, 200) + messages = list(get_messages(response.wsgi_request)) + self.assertEqual(len(messages), 0) + + def test_privacy_notice_middleware(self): + self.test_add_privacy_notice() + + self.login_as_admin() + #assign privacy notice + pn = PrivacyNotice.objects.all()[0] + study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] + study.acceptance_of_study_privacy_notice_required = True + study.study_privacy_notice = pn + study.save() + + study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] + self.assertEqual(study.acceptance_of_study_privacy_notice_required, True) + self.assertEqual(study.study_privacy_notice.id, pn.id) + + self.login_as_staff() + self.assertEqual(self.staff_worker.privacy_notice_accepted, False) + page = reverse('web.views.appointments') + response = self.client.get(page) + self.assertEqual(response.status_code, 302) + messages = list(get_messages(response.wsgi_request)) + self.assertEqual(len(messages), 1) + self.assertEqual(str(messages[0]), "You can't use the system until you accept the privacy notice.") + #accept privacy notice + form_data = dict(privacy_notice_accepted=True) + form = WorkerAcceptPrivacyNoticeForm(form_data) + self.assertTrue(form.is_valid()) + page = reverse('web.views.accept_privacy_notice', kwargs={'pk': pn.id}) + response = self.client.post(page, data={**form_data}) + self.assertEqual(response.status_code, 302) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertIn("Privacy notice accepted", messages) + #check acceptance + worker = Worker.objects.filter(id=self.staff_worker.id).first() + self.assertEqual(worker.privacy_notice_accepted, True) + page = reverse('web.views.appointments') + response = self.client.get(page) + self.assertEqual(response.status_code, 200) + messages = list(get_messages(response.wsgi_request)) + worker = Worker.get_by_user(response.wsgi_request.user) + self.assertEqual(worker.privacy_notice_accepted, True) diff --git a/smash/web/urls.py b/smash/web/urls.py index ce04e3b0b6fbcbf1101339161785ebafa384b049..1b76de8eaf3b42f2ce08b31e57164ea92cc0dd83 100644 --- a/smash/web/urls.py +++ b/smash/web/urls.py @@ -169,6 +169,19 @@ urlpatterns = [ url(r'^equipment_and_rooms/rooms/delete/(?P<room_id>\d+)$', views.rooms.rooms_delete, name='web.views.equipment_and_rooms.rooms_delete'), + #################### + # PRIVACY NOTICE # + #################### + + url(r'^privacy_notices$', views.privacy_notice.PrivacyNoticeListView.as_view(), name='web.views.privacy_notices'), + + url(r'^accept_privacy_notice/(?P<pk>\d+)$', views.privacy_notice.privacy_notice_accept, name='web.views.accept_privacy_notice'), + + url(r'^privacy_notices/add$', views.privacy_notice.privacy_notice_add, name='web.views.privacy_notice_add'), + url(r'^privacy_notices/(?P<pk>\d+)/edit$', views.privacy_notice.privacy_notice_edit, name='web.views.privacy_notice_edit'), + + url(r'^privacy_notices/(?P<pk>\d+)/delete$', views.privacy_notice.PrivacyNoticeDeleteView.as_view(), name='web.views.privacy_notice_delete'), + #################### # MAIL # #################### diff --git a/smash/web/views/__init__.py b/smash/web/views/__init__.py index 90f254227fb955480ca1425d8a6a4764dd004ca6..779ba85535665619709884c836ee7480316f1aea 100644 --- a/smash/web/views/__init__.py +++ b/smash/web/views/__init__.py @@ -1,5 +1,6 @@ # coding=utf-8 from django.conf import settings +from django.http import HttpRequest from django.shortcuts import redirect, render from django.views.generic.base import ContextMixin @@ -41,13 +42,16 @@ def wrap_response(request, template, params): return render(request, template, final_params) -def extend_context(params, request): +def extend_context(params, request: HttpRequest): study = Study.get_by_id(GLOBAL_STUDY_ID) person = Worker.get_by_user(request.user) # None if AnonymousUser or no Worker associated permissions = set() + show_notice = True if person is not None: role = person.role permissions = person.get_permissions(study) + show_notice = study.study_privacy_notice \ + and not person.privacy_notice_accepted person = str(person) else: #use full name if available, username otherwise @@ -56,6 +60,8 @@ def extend_context(params, request): else: person = request.user.get_username() role = '<No worker information>' + if request.resolver_match is not None and request.resolver_match.url_name == 'web.views.accept_privacy_notice': + show_notice = False notifications = get_notifications(request.user) final_params = params.copy() final_params.update({ @@ -65,6 +71,7 @@ def extend_context(params, request): 'person': person, 'role': role, 'notifications': notifications, + 'show_notice' : show_notice, 'study_id': GLOBAL_STUDY_ID, 'study' : study }) @@ -105,4 +112,5 @@ from . import uploaded_files from . import study from . import password from . import appointment_type -from . import provenance \ No newline at end of file +from . import provenance +from . import privacy_notice \ No newline at end of file diff --git a/smash/web/views/privacy_notice.py b/smash/web/views/privacy_notice.py new file mode 100644 index 0000000000000000000000000000000000000000..dc44ade1a75b40d399528b4d13bedff39e38125c --- /dev/null +++ b/smash/web/views/privacy_notice.py @@ -0,0 +1,108 @@ +# coding=utf-8 +import io +from wsgiref.util import FileWrapper + +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import redirect, get_object_or_404 +from django.urls import reverse_lazy +from django.views.generic import DeleteView +from django.views.generic import ListView + +from web.decorators import PermissionDecorator +from . import WrappedView +from . import wrap_response +from ..forms.privacy_notice import PrivacyNoticeForm +from ..forms.worker_form import WorkerAcceptPrivacyNoticeForm +from ..models import PrivacyNotice, Worker + + +class PrivacyNoticeListView(ListView, WrappedView): + model = PrivacyNotice + context_object_name = "privacy_notices" + template_name = 'privacy_notice/list.html' + + @PermissionDecorator('change_privacynotice', 'privacynotice') + def dispatch(self, *args, **kwargs): + return super(PrivacyNoticeListView, self).dispatch(*args, **kwargs) + + +@PermissionDecorator('change_privacynotice', 'privacynotice') +def privacy_notice_add(request): + if request.method == 'POST': + form = PrivacyNoticeForm(request.POST, request.FILES) + if form.is_valid(): + try: + form.save() + except: + messages.add_message(request, messages.ERROR, 'There was a problem when saving privacy notice. ' + 'Contact system administrator.') + return redirect('web.views.privacy_notices') + else: + form = PrivacyNoticeForm() + + return wrap_response(request, 'privacy_notice/add.html', {'form': form}) + + +@PermissionDecorator('change_privacynotice', 'privacynotice') +def privacy_notice_edit(request, pk): + privacy_notice = get_object_or_404(PrivacyNotice, pk=pk) + if request.method == 'POST': + form = PrivacyNoticeForm(request.POST, request.FILES, instance=privacy_notice) + if form.is_valid(): + try: + form.save() + return redirect('web.views.privacy_notices') + except: + messages.add_message(request, messages.ERROR, 'There was a problem when updating the privacy notice.' + 'Contact system administrator.') + return wrap_response(request, 'privacy_notice/edit.html', + {'form': form, 'privacy_notice': privacy_notice}) + else: + form = PrivacyNoticeForm(instance=privacy_notice) + + return wrap_response(request, 'privacy_notice/edit.html', {'form': form, 'privacy_notice': privacy_notice}) + + +class PrivacyNoticeDeleteView(DeleteView, WrappedView): + model = PrivacyNotice + success_url = reverse_lazy('web.views.privacy_notices') + template_name = 'privacy_notice/confirm_delete.html' + + @PermissionDecorator('change_privacynotice', 'privacynotice') + def delete(self, request, *args, **kwargs): + messages.success(request, "Privacy Notice deleted") + # try: + return super(PrivacyNoticeDeleteView, self).delete(request, *args, **kwargs) + # except: + # messages.add_message(request, messages.ERROR, 'There was a problem when deleting privacy notice. ' + # 'Contact system administrator.') + return redirect('web.views.privacy_notices') + + +def privacy_notice_accept(request, pk): + privacy_notice = get_object_or_404(PrivacyNotice, pk=pk) + worker = Worker.get_by_user(request.user) + if request.method == 'POST': + form = WorkerAcceptPrivacyNoticeForm(request.POST, instance=worker) + if form.is_valid(): + # noinspection PyBroadException + try: + form.save() + if form.cleaned_data['privacy_notice_accepted']: + messages.add_message(request, messages.SUCCESS, 'Privacy notice accepted') + if request.POST.get('next'): + return redirect(request.POST.get('next')) + return redirect('web.views.appointments') + else: + return redirect('logout') + except BaseException: + messages.add_message(request, messages.ERROR, 'There was a problem when updating the privacy notice.' + 'Contact system administrator.') + return wrap_response(request, 'privacy_notice/acceptance_study_privacy_notice.html', + {'form': form, 'privacy_notice': privacy_notice}) + else: + form = WorkerAcceptPrivacyNoticeForm(instance=worker) + + return wrap_response(request, 'privacy_notice/acceptance_study_privacy_notice.html', + {'form': form, 'privacy_notice': privacy_notice}) diff --git a/smash/web/views/study.py b/smash/web/views/study.py index 8d8bc07de6ef171d55ed5b4a21065e5af95bca72..7060a98284ba20e9b8f99b9795c0a31591d2311f 100644 --- a/smash/web/views/study.py +++ b/smash/web/views/study.py @@ -53,6 +53,7 @@ def study_edit(request, study_id): return wrap_response(request, 'study/edit.html', { 'study_form': study_form, + 'privacy_notice': study.study_privacy_notice, 'notifications_form': notifications_form, 'study_columns_form': study_columns_form, 'redcap_columns_form': redcap_columns_form