diff --git a/castellum/castellum_auth/fixtures/groups.json b/castellum/castellum_auth/fixtures/groups.json
index bbbe6e54483045fad824a04f3d85624fe4129119..0760eefac6d1db28648b4fe82049614d65737717 100644
--- a/castellum/castellum_auth/fixtures/groups.json
+++ b/castellum/castellum_auth/fixtures/groups.json
@@ -52,6 +52,11 @@
"recruit",
"recruitment",
"participation"
+ ],
+ [
+ "change_appointment",
+ "recruitment",
+ "appointment"
]
]
}
@@ -66,6 +71,11 @@
"conduct_study",
"recruitment",
"participation"
+ ],
+ [
+ "change_appointment",
+ "recruitment",
+ "appointment"
]
]
}
diff --git a/castellum/execution/templates/execution/participation_appointments.html b/castellum/execution/templates/execution/participation_appointments.html
index aee26fd53e86f5153f0fde9d6317cf958ee2cbdf..a2b71a553853beda962278f9a47e5210703a7c09 100644
--- a/castellum/execution/templates/execution/participation_appointments.html
+++ b/castellum/execution/templates/execution/participation_appointments.html
@@ -1,5 +1,5 @@
{% extends "execution/participation_base.html" %}
-{% load i18n bootstrap4 utils %}
+{% load i18n auth bootstrap4 utils %}
{% block content %}
@@ -25,7 +25,10 @@
{% endfor %}
-
+ {% has_perm 'recruitment.change_appointment' user study as can_change_appointment %}
+ {% if can_change_appointment %}
+
+ {% endif %}
{% endblock %}
diff --git a/castellum/execution/templates/execution/participation_appointments_form.html b/castellum/execution/templates/execution/participation_appointments_form.html
index faa487f0ba0870db13dd294bf41201592502375b..be89cbb7c9e9d9d8d17eb5075559fdf36cca6cd5 100644
--- a/castellum/execution/templates/execution/participation_appointments_form.html
+++ b/castellum/execution/templates/execution/participation_appointments_form.html
@@ -27,7 +27,7 @@
{% endfor %}
diff --git a/castellum/execution/templates/execution/participation_base.html b/castellum/execution/templates/execution/participation_base.html
index 5b86d58dd24056dbf5d214981a58a1f2afe63e51..1ba76987bbaca0fc10495c950064966f956475f7 100644
--- a/castellum/execution/templates/execution/participation_base.html
+++ b/castellum/execution/templates/execution/participation_base.html
@@ -1,5 +1,5 @@
{% extends "execution/base.html" %}
-{% load i18n auth %}
+{% load i18n %}
{% block title %}{{ study }} · {{ block.super }}{% endblock %}
diff --git a/castellum/execution/views.py b/castellum/execution/views.py
index b8bdd2e76ecc59454730eca890dd2edf63256ea8..e5c64133c1eb45e3f4c084f3c526fc98823b852e 100644
--- a/castellum/execution/views.py
+++ b/castellum/execution/views.py
@@ -142,6 +142,7 @@ class ParticipationAppointmentsView(ParticipationDetailMixin, DetailView):
class ParticipationAppointmentsUpdateView(ParticipationDetailMixin, UpdateView):
template_name = 'execution/participation_appointments_form.html'
+ permission_required = ['recruitment.conduct_study', 'recruitment.change_appointment']
form_class = AppointmentsForm
tab = 'appointments'
diff --git a/castellum/recruitment/feeds.py b/castellum/recruitment/feeds.py
index ba551e249ee70184a304069b98e41596a1f93bd5..b6f22d689c46960d99c049707dc40f0e8f0522e8 100644
--- a/castellum/recruitment/feeds.py
+++ b/castellum/recruitment/feeds.py
@@ -62,7 +62,7 @@ class FollowUpFeedForUser(BaseFollowUpFeed):
class AppointmentFeed(BaseAppointmentFeed):
def get_object(self, request, *args, **kwargs):
study = get_object_or_404(Study, pk=kwargs['pk'], status=Study.EXECUTION)
- perms = ['recruitment.recruit', 'studies.access_study']
+ perms = ['recruitment.change_appointment', 'studies.access_study']
if not self.request.user.has_perms(perms, obj=study):
raise PermissionDenied
return study
diff --git a/castellum/recruitment/forms.py b/castellum/recruitment/forms.py
index 5f20df56771f32b0bfa05e4e6d4d2b8ffac64f4e..2918ecbdc7986f389988e7fed8a8dab5d177d94a 100644
--- a/castellum/recruitment/forms.py
+++ b/castellum/recruitment/forms.py
@@ -267,7 +267,7 @@ class AppointmentsForm(forms.ModelForm):
yield self[name]
-class ContactForm(AppointmentsForm):
+class ContactForm(forms.ModelForm):
class Meta:
model = Participation
fields = ['status', 'followup_date', 'followup_time', 'exclusion_criteria_checked']
@@ -290,6 +290,10 @@ class ContactForm(AppointmentsForm):
self.fields['status'].widget.disabled_choices = [Participation.INVITED]
+class ContactAndAppointmentsForm(ContactForm, AppointmentsForm):
+ pass
+
+
class SendMailForm(forms.Form):
batch_size = forms.IntegerField(
min_value=1, label=_('How many subjects do you want to contact?')
diff --git a/castellum/recruitment/templates/recruitment/contact.html b/castellum/recruitment/templates/recruitment/contact.html
index 6d06ce8ef2644899c4770c394afad9aa30afc71c..ac8c55204be6fd2583d5d65cecdca01331caf551 100644
--- a/castellum/recruitment/templates/recruitment/contact.html
+++ b/castellum/recruitment/templates/recruitment/contact.html
@@ -36,6 +36,8 @@
{% endif %}
+ {% has_perm 'recruitment.change_appointment' user study as can_change_appointment %}
+
-
{% translate "Contacting details" %}
@@ -51,7 +53,7 @@
{% endif %}
- {% if study.studysession_set.exists %}
+ {% if can_change_appointment and study.studysession_set.exists %}
-
{% translate 'Appointments' %}
@@ -160,28 +162,30 @@
{% endif %}
-
- {% if study.session_instructions %}
- {{ study.session_instructions|linebreaks }}
- {% endif %}
+ {% if can_change_appointment %}
+
+ {% if study.session_instructions %}
+ {{ study.session_instructions|linebreaks }}
+ {% endif %}
-
- - {{ study|verbose_name:'sessions_start' }}
- -
-
-
+
+ - {{ study|verbose_name:'sessions_start' }}
+ -
+
+
- - {{ study|verbose_name:'sessions_end' }}
- -
-
-
-
+ - {{ study|verbose_name:'sessions_end' }}
+ -
+
+
+
- {% for field in form.appointments %}
- {% bootstrap_field field %}
- {% endfor %}
-
{% translate 'Calendar' %}
-
+ {% for field in form.appointments %}
+ {% bootstrap_field field %}
+ {% endfor %}
+
{% translate 'Calendar' %}
+
+ {% endif %}
{% include 'utils/form_errors.html' with form=form %}
diff --git a/castellum/recruitment/views.py b/castellum/recruitment/views.py
index f0ac228fc4f7ea283ae20aa25886b0bb5ecdf662..e9b87d254b5d944c39bfd6055b0af2cce7487e6a 100644
--- a/castellum/recruitment/views.py
+++ b/castellum/recruitment/views.py
@@ -45,6 +45,7 @@ from castellum.subjects.mixins import BaseDataProtectionUpdateView
from castellum.utils import cached_request
from castellum.utils import contrast_color
+from .forms import ContactAndAppointmentsForm
from .forms import ContactForm
from .mixins import BaseAttributeSetUpdateView
from .mixins import BaseCalendarView
@@ -284,11 +285,16 @@ class MailRecruitmentView(BaseMailRecruitmentView):
class ContactView(ParticipationMixin, PermissionRequiredMixin, UpdateView):
model = Participation
- form_class = ContactForm
template_name = 'recruitment/contact.html'
permission_required = 'recruitment.recruit'
study_status = [Study.EXECUTION]
+ def get_form_class(self):
+ if self.request.user.has_perm('recruitment.change_appointment', self.study):
+ return ContactAndAppointmentsForm
+ else:
+ return ContactForm
+
def get_initial(self):
initial = super().get_initial()
if not self.object.match:
@@ -321,7 +327,8 @@ class ContactView(ParticipationMixin, PermissionRequiredMixin, UpdateView):
def form_valid(self, form):
response = super().form_valid(form)
- send_appointment_notifications(self.request, self.object, form.appointment_changes)
+ if isinstance(form, ContactAndAppointmentsForm):
+ send_appointment_notifications(self.request, self.object, form.appointment_changes)
return response
@@ -359,7 +366,7 @@ class AdditionalInfoUpdateView(RecruitmentUpdateMixin, BaseAdditionalInfoUpdateV
class CalendarView(StudyMixin, PermissionRequiredMixin, BaseCalendarView):
model = Study
- permission_required = 'recruitment.recruit'
+ permission_required = 'recruitment.change_appointment'
study_status = [Study.EXECUTION]
nochrome = True
feed = 'recruitment:calendar-feed'
diff --git a/tests/recruitment/views/test_contact_view.py b/tests/recruitment/views/test_contact_view.py
index bb2c360ec5fc5b50d236390d81190dbfe0174262..ef2b591d975888d2a98134f8c872da2fc7b14686 100644
--- a/tests/recruitment/views/test_contact_view.py
+++ b/tests/recruitment/views/test_contact_view.py
@@ -1,12 +1,17 @@
+import datetime
+
+from django.contrib.auth.models import Permission
from django.utils import timezone
import pytest
from model_bakery import baker
+from castellum.castellum_auth.models import User
from castellum.recruitment.models import Appointment
from castellum.recruitment.models import Participation
from castellum.studies.models import Resource
from castellum.studies.models import Study
+from castellum.studies.models import StudyMembership
from castellum.studies.models import StudySession
@@ -194,3 +199,33 @@ def test_appointment_resource_overlap_ignore_not_invited(client, member, partici
'appointment-%i_1' % session.pk: '12:00',
})
assert response.status_code == 302
+
+
+def test_appointment_change_appointment_permission(client, member, participation):
+ future = timezone.now() + datetime.timedelta(days=10000)
+ user = baker.make(User, expiration_date=future, email='test@example.com')
+ user.user_permissions.add(Permission.objects.get(codename='privacy_level_1'))
+ user.user_permissions.add(Permission.objects.get(codename='recruit'))
+ StudyMembership.objects.create(user=user, study=participation.study)
+
+ session = baker.make(StudySession, study=participation.study, duration=60)
+
+ url = '/recruitment/{}/{}/'.format(participation.study.pk, participation.pk)
+
+ client.force_login(user)
+ response = client.post(url, {
+ 'status': 2,
+ 'appointment-%i_0' % session.pk: '2020-01-01',
+ 'appointment-%i_1' % session.pk: '12:00',
+ })
+ assert response.status_code == 302
+ assert not participation.appointment_set.exists()
+
+ client.force_login(member)
+ response = client.post(url, {
+ 'status': 2,
+ 'appointment-%i_0' % session.pk: '2020-01-01',
+ 'appointment-%i_1' % session.pk: '12:00',
+ })
+ assert response.status_code == 302
+ assert participation.appointment_set.exists()