diff --git a/amy/consents/migrations/0008_trainingrequestconsent.py b/amy/consents/migrations/0008_trainingrequestconsent.py
new file mode 100644
index 000000000..8feb5117e
--- /dev/null
+++ b/amy/consents/migrations/0008_trainingrequestconsent.py
@@ -0,0 +1,31 @@
+# Generated by Django 3.2.18 on 2023-03-11 15:26
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('workshops', '0256_auto_20230220_2051'),
+ ('consents', '0007_term_short_description'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='TrainingRequestConsent',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('last_updated_at', models.DateTimeField(auto_now=True, null=True)),
+ ('archived_at', models.DateTimeField(blank=True, null=True)),
+ ('term', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='consents.term')),
+ ('term_option', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='consents.termoption')),
+ ('training_request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='workshops.trainingrequest')),
+ ],
+ ),
+ migrations.AddConstraint(
+ model_name='trainingrequestconsent',
+ constraint=models.UniqueConstraint(condition=models.Q(('archived_at__isnull', True)), fields=('training_request', 'term'), name='training_request__term__unique__when__archived_at__null'),
+ ),
+ ]
diff --git a/amy/consents/models.py b/amy/consents/models.py
index 6e9aa4e60..1986be37c 100644
--- a/amy/consents/models.py
+++ b/amy/consents/models.py
@@ -12,7 +12,7 @@
from autoemails.mixins import RQJobsMixin
from consents.exceptions import TermOptionDoesNotBelongToTermException
from workshops.mixins import CreatedUpdatedArchivedMixin
-from workshops.models import STR_LONG, STR_MED, Person
+from workshops.models import STR_LONG, STR_MED, Person, TrainingRequest
class TermQuerySet(QuerySet):
@@ -202,20 +202,13 @@ def active(self):
return self.filter(archived_at=None)
-class Consent(CreatedUpdatedArchivedMixin, models.Model):
- person = models.ForeignKey(Person, on_delete=models.CASCADE)
+class BaseConsent(CreatedUpdatedArchivedMixin, models.Model):
term = models.ForeignKey(Term, on_delete=models.PROTECT)
term_option = models.ForeignKey(TermOption, on_delete=models.PROTECT, null=True)
objects = Manager.from_queryset(ConsentQuerySet)()
class Meta:
- constraints = [
- models.UniqueConstraint(
- fields=["person", "term"],
- name="person__term__unique__when__archived_at__null",
- condition=models.Q(archived_at__isnull=True),
- ),
- ]
+ abstract = True
def save(self, *args, **kwargs):
if self.term_option and self.term.pk != self.term_option.term.pk:
@@ -230,6 +223,23 @@ def is_archived(self) -> bool:
def is_active(self) -> bool:
return self.archived_at is None
+ def archive(self) -> None:
+ self.archived_at = timezone.now()
+ self.save()
+
+
+class Consent(BaseConsent):
+ person = models.ForeignKey(Person, on_delete=models.CASCADE)
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=["person", "term"],
+ name="person__term__unique__when__archived_at__null",
+ condition=models.Q(archived_at__isnull=True),
+ ),
+ ]
+
@classmethod
def create_unset_consents_for_term(cls, term: Term) -> None:
"""
@@ -247,10 +257,6 @@ def create_unset_consents_for_term(cls, term: Term) -> None:
for person in Person.objects.all()
)
- def archive(self) -> None:
- self.archived_at = timezone.now()
- self.save()
-
@classmethod
def archive_all_for_term(cls, terms: Iterable[Term]) -> None:
consents = cls.objects.filter(term__in=terms).active()
@@ -282,3 +288,21 @@ def reconsent(consent: Consent, term_option: TermOption) -> "Consent":
term_option=term_option,
person_id=consent.person.pk,
)
+
+
+class TrainingRequestConsent(BaseConsent):
+ """
+ A consent for a training request. People filling out the training request form
+ should accept all required consents. This model is used to store the consents.
+ """
+
+ training_request = models.ForeignKey(TrainingRequest, on_delete=models.CASCADE)
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=["training_request", "term"],
+ name="training_request__term__unique__when__archived_at__null",
+ condition=models.Q(archived_at__isnull=True),
+ ),
+ ]
diff --git a/amy/extforms/forms.py b/amy/extforms/forms.py
index 33ae3465d..d1b636a4c 100644
--- a/amy/extforms/forms.py
+++ b/amy/extforms/forms.py
@@ -1,8 +1,13 @@
+from typing import Iterable, cast
+
from captcha.fields import ReCaptchaField
-from crispy_forms.layout import HTML, Div, Field
+from crispy_forms.layout import HTML, Div, Field, Layout
from django import forms
from django.core.exceptions import ValidationError
+from django.db.models.fields import BLANK_CHOICE_DASH
+from consents.forms import option_display_value
+from consents.models import Term, TrainingRequestConsent
from extrequests.forms import (
SelfOrganisedSubmissionBaseForm,
WorkshopInquiryRequestBaseForm,
@@ -59,7 +64,7 @@ class Meta:
"max_travelling_frequency_other",
"reason",
"user_notes",
- "data_privacy_agreement",
+ # "data_privacy_agreement",
"code_of_conduct_agreement",
"training_completion_agreement",
"workshop_teaching_agreement",
@@ -85,54 +90,70 @@ class Meta:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+ # Only active and required terms.
+ self.terms = (
+ Term.objects.prefetch_active_options()
+ .filter(required_type=Term.PROFILE_REQUIRE_TYPE)
+ .order_by("slug")
+ )
+
+ self.set_consent_fields(self.terms)
+
# set up a layout object for the helper
self.helper.layout = self.helper.build_default_layout(self)
- # set up RadioSelectWithOther widget so that it can display additional
- # field inline
- self["occupation"].field.widget.other_field = self["occupation_other"]
- self["domains"].field.widget.other_field = self["domains_other"]
- self["previous_training"].field.widget.other_field = self[
- "previous_training_other"
- ]
- self["previous_experience"].field.widget.other_field = self[
- "previous_experience_other"
- ]
- self["teaching_frequency_expectation"].field.widget.other_field = self[
- "teaching_frequency_expectation_other"
- ]
- self["max_travelling_frequency"].field.widget.other_field = self[
- "max_travelling_frequency_other"
+ self.set_other_fields(self.helper.layout)
+ self.set_fake_required_fields()
+ self.set_accordion(self.helper.layout)
+ self.set_hr(self.helper.layout)
+
+ def set_other_field(self, field_name: str, layout: Layout) -> None:
+ """
+ Set up a field so that it can be displayed as a separate widget.
+ """
+ WidgetType = self._meta.widgets[field_name].__class__ # type: ignore
+ cast(WidgetType, self[field_name].field.widget).other_field = self[
+ f"{field_name}_other"
]
-
- # remove that additional field
- self.helper.layout.fields.remove("occupation_other")
- self.helper.layout.fields.remove("domains_other")
- self.helper.layout.fields.remove("previous_training_other")
- self.helper.layout.fields.remove("previous_experience_other")
- self.helper.layout.fields.remove("teaching_frequency_expectation_other")
- self.helper.layout.fields.remove("max_travelling_frequency_other")
-
+ layout.fields.remove(f"{field_name}_other")
+
+ def set_other_fields(self, layout: Layout) -> None:
+ """
+ Set fields that have "Other" counterpart as a separate widget.
+ """
+ # Set up "*WithOther" widgets so that they can display additional
+ # inline fields. The original "*other" fields are removed from the layout.
+ self.set_other_field("occupation", layout)
+ self.set_other_field("domains", layout)
+ self.set_other_field("previous_training", layout)
+ self.set_other_field("previous_experience", layout)
+ self.set_other_field("teaching_frequency_expectation", layout)
+ self.set_other_field("max_travelling_frequency", layout)
+
+ def set_fake_required_fields(self) -> None:
# fake requiredness of the registration code / group name
- self["group_name"].field.widget.fake_required = True
+ self["group_name"].field.widget.fake_required = True # type: ignore
+ def set_accordion(self, layout: Layout) -> None:
# special accordion display for the review process
- self["review_process"].field.widget.subfields = {
+ self["review_process"].field.widget.subfields = { # type: ignore
"preapproved": [
self["group_name"],
],
"open": [], # this option doesn't require any additional fields
}
- self["review_process"].field.widget.notes = TrainingRequest.REVIEW_CHOICES_NOTES
+ self[
+ "review_process"
+ ].field.widget.notes = TrainingRequest.REVIEW_CHOICES_NOTES # type: ignore
# get current position of `review_process` field
- pos_index = self.helper.layout.fields.index("review_process")
+ pos_index = layout.fields.index("review_process")
- self.helper.layout.fields.remove("review_process")
- self.helper.layout.fields.remove("group_name")
+ layout.fields.remove("review_process")
+ layout.fields.remove("group_name")
# insert div+field at previously saved position
- self.helper.layout.insert(
+ layout.insert(
pos_index,
Div(
Field(
@@ -142,12 +163,33 @@ def __init__(self, *args, **kwargs):
),
)
+ def set_hr(self, layout: Layout) -> None:
# add
around "underrepresented*" fields
- index = self.helper.layout.fields.index("underrepresented")
- self.helper.layout.insert(index, HTML(self.helper.hr()))
-
- index = self.helper.layout.fields.index("underrepresented_details")
- self.helper.layout.insert(index + 1, HTML(self.helper.hr()))
+ index = layout.fields.index("underrepresented")
+ layout.insert(index, HTML(self.helper.hr()))
+
+ index = layout.fields.index("underrepresented_details")
+ layout.insert(index + 1, HTML(self.helper.hr()))
+
+ def set_consent_fields(self, terms: Iterable[Term]) -> None:
+ for term in terms:
+ self.fields[term.slug] = self.create_consent_field(term)
+
+ def create_consent_field(self, term: Term) -> forms.ChoiceField:
+ options = [(opt.pk, option_display_value(opt)) for opt in term.options]
+ required = term.required_type == Term.PROFILE_REQUIRE_TYPE
+ initial = None
+ attrs = {"class": "border border-warning"} if initial is None else {}
+
+ field = forms.ChoiceField(
+ choices=BLANK_CHOICE_DASH + options,
+ label=term.content,
+ required=required,
+ initial=initial,
+ help_text=term.help_text or "",
+ widget=forms.Select(attrs=attrs),
+ )
+ return field
def clean(self):
super().clean()
@@ -174,6 +216,23 @@ def clean(self):
if errors:
raise ValidationError(errors)
+ def save(self, *args, **kwargs) -> None:
+ training_request = super().save(*args, **kwargs)
+ new_consents: list[TrainingRequestConsent] = []
+ for term in self.terms:
+ option_id = self.cleaned_data.get(term.slug)
+ if not option_id:
+ continue
+ new_consents.append(
+ TrainingRequestConsent(
+ training_request=training_request,
+ term_option_id=option_id,
+ term_id=term.pk,
+ )
+ )
+ TrainingRequestConsent.objects.bulk_create(new_consents)
+ return training_request
+
class WorkshopRequestExternalForm(WorkshopRequestBaseForm):
captcha = ReCaptchaField()
diff --git a/amy/extforms/tests/test_training_request_form.py b/amy/extforms/tests/test_training_request_form.py
index ab0996f49..5204dd337 100644
--- a/amy/extforms/tests/test_training_request_form.py
+++ b/amy/extforms/tests/test_training_request_form.py
@@ -1,6 +1,7 @@
from django.core import mail
from django.urls import reverse
+from consents.models import Term, TermOptionChoices
from extforms.views import TrainingRequestCreate
from workshops.models import Role, TrainingRequest
from workshops.tests.base import TestBase
@@ -45,6 +46,23 @@ def setUp(self):
"agreed_to_teach_workshops": "on",
"privacy_consent": True,
}
+ self.data.update(self.add_terms_to_payload())
+
+ def add_terms_to_payload(self) -> dict[str, int]:
+ data = {}
+ terms = (
+ Term.objects.prefetch_active_options()
+ .filter(required_type=Term.PROFILE_REQUIRE_TYPE)
+ .order_by("slug")
+ )
+ for term in terms:
+ option = next(
+ option
+ for option in term.options
+ if option.option_type == TermOptionChoices.AGREE
+ )
+ data[term.slug] = option.pk
+ return data
def test_request_added(self):
email = "john@smith.com"
diff --git a/amy/extrequests/forms.py b/amy/extrequests/forms.py
index 92815b7d2..b5ff1bc0e 100644
--- a/amy/extrequests/forms.py
+++ b/amy/extrequests/forms.py
@@ -1590,3 +1590,8 @@ class TrainingRequestsMergeForm(forms.Form):
initial=DEFAULT,
widget=forms.RadioSelect,
)
+ trainingrequestconsent_set = forms.ChoiceField(
+ choices=TWO,
+ initial=DEFAULT,
+ widget=forms.RadioSelect,
+ )
diff --git a/amy/extrequests/tests/test_training_request.py b/amy/extrequests/tests/test_training_request.py
index 54dd5d9de..596d15599 100644
--- a/amy/extrequests/tests/test_training_request.py
+++ b/amy/extrequests/tests/test_training_request.py
@@ -10,7 +10,14 @@
from django.urls import reverse
from django_comments.models import Comment
-from consents.models import Consent, Term, TermEnum, TermOptionChoices
+from consents.models import (
+ Consent,
+ Term,
+ TermEnum,
+ TermOption,
+ TermOptionChoices,
+ TrainingRequestConsent,
+)
from extrequests.forms import TrainingRequestsMergeForm
from extrequests.views import _match_training_request_to_person
from workshops.models import (
@@ -677,21 +684,6 @@ def test_matching_with_existing_account_works(self):
getattr(self.ironman, key), value, "Attribute: {}".format(key)
)
- # Ensure new style consents were created
- Consent.objects.active().get(
- person=self.ironman,
- term=Term.objects.get_by_key(TermEnum.MAY_CONTACT),
- # may-contact defaults to AGREE for matching training request to a person
- term_option__option_type=TermOptionChoices.AGREE,
- )
- Consent.objects.active().get(
- person=self.ironman,
- term=Term.objects.get_by_key(TermEnum.PRIVACY_POLICY),
- term_option__option_type=TermOptionChoices.AGREE
- if req.data_privacy_agreement
- else TermOptionChoices.DECLINE,
- )
-
self.assertEqual(set(self.ironman.domains.all()), set(req.domains.all()))
def test_matching_with_new_account_works(self):
@@ -724,23 +716,62 @@ def test_matching_with_new_account_works(self):
getattr(req.person, key), value, "Attribute: {}".format(key)
)
- # Ensure new style consents were created
+ self.assertEqual(set(req.person.domains.all()), set(req.domains.all()))
+
+ def test_matching_updates_consents(self) -> None:
+ # Arrange
+ req = create_training_request(state="p", person=None)
+ may_contact_term = Term.objects.get_by_key(TermEnum.MAY_CONTACT)
+ privacy_policy_term = Term.objects.get_by_key(TermEnum.PRIVACY_POLICY)
+ public_profile_term = Term.objects.get_by_key(TermEnum.PUBLIC_PROFILE)
+ TrainingRequestConsent.objects.create(
+ training_request=req,
+ term=may_contact_term,
+ term_option=TermOption.objects.filter(
+ term=may_contact_term
+ ).get_decline_term_option(),
+ )
+ TrainingRequestConsent.objects.create(
+ training_request=req,
+ term=privacy_policy_term,
+ term_option=TermOption.objects.filter(
+ term=privacy_policy_term
+ ).get_agree_term_option(),
+ )
+ TrainingRequestConsent.objects.create(
+ training_request=req,
+ term=public_profile_term,
+ term_option=TermOption.objects.filter(
+ term=public_profile_term
+ ).get_agree_term_option(),
+ )
+
+ # Act
+ rv = self.client.post(
+ reverse("trainingrequest_details", args=[req.pk]),
+ data={"person": self.ironman.pk, "match-selected-person": ""},
+ follow=True,
+ )
+
+ # Assert
+ self.assertEqual(rv.status_code, 200)
+ req.refresh_from_db()
Consent.objects.active().get(
person=req.person,
- term=Term.objects.get_by_key(TermEnum.MAY_CONTACT),
- # may-contact defaults to AGREE for matching training request to a person
+ term=may_contact_term,
+ term_option__option_type=TermOptionChoices.DECLINE,
+ )
+ Consent.objects.active().get(
+ person=req.person,
+ term=privacy_policy_term,
term_option__option_type=TermOptionChoices.AGREE,
)
Consent.objects.active().get(
person=req.person,
- term=Term.objects.get_by_key(TermEnum.PRIVACY_POLICY),
- term_option__option_type=TermOptionChoices.AGREE
- if req.data_privacy_agreement
- else TermOptionChoices.DECLINE,
+ term=public_profile_term,
+ term_option__option_type=TermOptionChoices.AGREE,
)
- self.assertEqual(set(req.person.domains.all()), set(req.domains.all()))
-
def test_matching_in_transaction(self):
"""This is a regression test.
@@ -785,7 +816,7 @@ def test_matching_in_transaction(self):
# matching fails because it can't rewrite email address due to
# uniqueness constraint
self.assertFalse(_match_training_request_to_person(request, tr, person, create))
- messages = request._messages._queued_messages
+ messages = request._messages._queued_messages # type: ignore
self.assertEqual(len(messages), 1)
self.assertEqual(messages[0].level, WARNING)
@@ -864,6 +895,39 @@ def setUp(self):
self.second_req.previous_involvement.set([self.helper])
self.third_req.previous_involvement.set([self.instructor, self.contributor])
+ # consents
+ may_contact_term = Term.objects.get_by_key(TermEnum.MAY_CONTACT)
+ privacy_policy_term = Term.objects.get_by_key(TermEnum.PRIVACY_POLICY)
+ public_profile_term = Term.objects.get_by_key(TermEnum.PUBLIC_PROFILE)
+ self.may_contact_consent = TrainingRequestConsent.objects.create(
+ training_request=self.first_req,
+ term=may_contact_term,
+ term_option=TermOption.objects.filter(
+ term=may_contact_term
+ ).get_decline_term_option(),
+ )
+ TrainingRequestConsent.objects.create(
+ training_request=self.third_req,
+ term=may_contact_term,
+ term_option=TermOption.objects.filter(
+ term=may_contact_term
+ ).get_agree_term_option(),
+ )
+ TrainingRequestConsent.objects.create(
+ training_request=self.third_req,
+ term=privacy_policy_term,
+ term_option=TermOption.objects.filter(
+ term=privacy_policy_term
+ ).get_agree_term_option(),
+ )
+ TrainingRequestConsent.objects.create(
+ training_request=self.third_req,
+ term=public_profile_term,
+ term_option=TermOption.objects.filter(
+ term=public_profile_term
+ ).get_agree_term_option(),
+ )
+
# prepare merge strategies (POST data to be sent to the merging view)
self.strategy_1 = {
"trainingrequest_a": self.first_req.pk,
@@ -909,6 +973,7 @@ def setUp(self):
"code_of_conduct_agreement": "obj_a",
"created_at": "obj_a",
"comments": "combine",
+ "trainingrequestconsent_set": "obj_a",
}
self.strategy_2 = {
"trainingrequest_a": self.first_req.pk,
@@ -954,6 +1019,7 @@ def setUp(self):
"code_of_conduct_agreement": "obj_a",
"created_at": "obj_a",
"comments": "combine",
+ "trainingrequestconsent_set": "obj_b",
}
base_url = reverse("trainingrequests_merge")
@@ -1016,6 +1082,7 @@ def test_form_invalid_values(self):
"data_privacy_agreement": "combine",
"code_of_conduct_agreement": "combine",
"created_at": "combine",
+ "trainingrequestconsent_set": "combine",
}
# fields additionally accepting "combine"
passing = {
@@ -1032,10 +1099,8 @@ def test_form_invalid_values(self):
form = TrainingRequestsMergeForm(data)
self.assertFalse(form.is_valid())
- for key in failing:
- self.assertIn(key, form.errors)
- for key in passing:
- self.assertNotIn(key, form.errors)
+ self.assertEqual(form.errors.keys(), failing.keys()) # the same keys
+ self.assertTrue(form.errors.keys().isdisjoint(passing.keys())) # no overlap
# make sure no fields are added without this test being updated
self.assertEqual(set(list(form.fields.keys())), set(list(data.keys())))
@@ -1107,6 +1172,7 @@ def test_merging_relational_attributes(self):
assertions = {
"domains": set([self.chemistry, self.physics]),
"previous_involvement": set([self.helper]),
+ "trainingrequestconsent_set": set([self.may_contact_consent]),
# comments are not relational, they're related via generic FKs,
# so they won't appear here
}
@@ -1152,6 +1218,13 @@ def test_merging(self):
roles_set = set([self.helper, self.instructor, self.contributor])
self.assertEqual(domains_set, set(self.third_req.domains.all()))
self.assertEqual(roles_set, set(self.third_req.previous_involvement.all()))
+ self.assertEqual(self.third_req.trainingrequestconsent_set.count(), 3)
+ self.assertTrue(
+ all(
+ consent.term_option.option_type == TermOptionChoices.AGREE
+ for consent in self.third_req.trainingrequestconsent_set.all()
+ )
+ )
# make sure no M2M related objects were removed from DB
self.chemistry.refresh_from_db()
@@ -1169,6 +1242,10 @@ def test_merging(self):
self.ironman.refresh_from_db()
self.spiderman.refresh_from_db()
+ # make sure the remaining consent from the first request was removed
+ with self.assertRaises(TrainingRequestConsent.DoesNotExist):
+ self.may_contact_consent.refresh_from_db()
+
def test_merging_comments_strategy1(self):
"""Ensure comments regarding persons are correctly merged using
`merge_objects`.
diff --git a/amy/extrequests/views.py b/amy/extrequests/views.py
index 683e3866a..ae448e819 100644
--- a/amy/extrequests/views.py
+++ b/amy/extrequests/views.py
@@ -20,7 +20,7 @@
from autoemails.base_views import ActionManageMixin
from autoemails.forms import GenericEmailScheduleForm
from autoemails.models import EmailTemplate, Trigger
-from consents.models import Term, TermEnum, TermOption, TermOptionChoices
+from consents.models import Term, TermOption, TrainingRequestConsent
from consents.util import reconsent_for_term_option_type
from extrequests.base_views import AMYCreateAndFetchObjectView, WRFInitial
from extrequests.filters import (
@@ -726,53 +726,30 @@ def _match_training_request_to_person(
)
return False
- # Create new style consents according to selection from the training request
- # form.
- try:
- option_type = (
- TermOptionChoices.AGREE
- if training_request.data_privacy_agreement
- else TermOptionChoices.DECLINE
- )
-
- reconsent_for_term_option_type(
- term_key=TermEnum.PRIVACY_POLICY,
- term_option_type=option_type,
- person=training_request.person,
- )
-
- except (Term.DoesNotExist, TermOption.DoesNotExist):
- logger.warning(
- f"Either Term {TermEnum.PRIVACY_POLICY} or its term option was not found, "
- "can't proceed."
- )
- messages.error(
- request,
- f"Error when setting person's consents. Term {TermEnum.PRIVACY_POLICY} or "
- "related term option may not exist.",
- )
- return False
-
- try:
- # If they created a training request, we assume they agreed to "may contact"
- # consent.
- reconsent_for_term_option_type(
- term_key=TermEnum.MAY_CONTACT,
- term_option_type=TermOptionChoices.AGREE,
- person=training_request.person,
- )
-
- except (Term.DoesNotExist, TermOption.DoesNotExist):
- logger.warning(
- f"Either Term {TermEnum.MAY_CONTACT} or its term option was not found, "
- "can't proceed."
- )
- messages.error(
- request,
- f"Error when setting person's consents. Term {TermEnum.MAY_CONTACT} or "
- "related term option may not exist.",
- )
- return False
+ # Create new style consents based on the training request consents used in the
+ # training request form.
+ training_request_consents = TrainingRequestConsent.objects.filter(
+ training_request=training_request
+ ).select_related("term_option", "term")
+ for consent in training_request_consents:
+ try:
+ option_type = consent.term_option.option_type
+ reconsent_for_term_option_type(
+ term_key=consent.term.key,
+ term_option_type=option_type,
+ person=training_request.person,
+ )
+ except (Term.DoesNotExist, TermOption.DoesNotExist):
+ logger.warning(
+ f"Either Term {consent.term.key} or its term option was not found, "
+ "can't proceed."
+ )
+ messages.error(
+ request,
+ f"Error when setting person's consents. Term {consent.term.key} or "
+ "related term option may not exist.",
+ )
+ return False
return True
@@ -825,10 +802,20 @@ def trainingrequest_details(request, pk):
).first() # may return None
form = MatchTrainingRequestForm(initial={"person": person})
+ TERM_SLUGS = ["may-contact", "privacy-policy", "public-profile"]
context = {
"title": "Training request #{}".format(req.pk),
"req": req,
"form": form,
+ "consents": {
+ consent.term.key: consent
+ for consent in TrainingRequestConsent.objects.select_related(
+ "term", "term_option"
+ ).filter(training_request=req)
+ },
+ "consents_content": {
+ term.key: term.content for term in Term.objects.filter(slug__in=TERM_SLUGS)
+ },
}
return render(request, "requests/trainingrequest.html", context)
@@ -933,6 +920,8 @@ def trainingrequests_merge(request):
difficult = (
"domains",
"previous_involvement",
+ "comments",
+ "trainingrequestconsent_set",
)
try:
@@ -963,6 +952,18 @@ def trainingrequests_merge(request):
"obj_a": obj_a,
"obj_b": obj_b,
"form": form,
+ "obj_a_consents": {
+ consent.term.key: consent
+ for consent in TrainingRequestConsent.objects.select_related(
+ "term", "term_option"
+ ).filter(training_request=obj_a)
+ },
+ "obj_b_consents": {
+ consent.term.key: consent
+ for consent in TrainingRequestConsent.objects.select_related(
+ "term", "term_option"
+ ).filter(training_request=obj_b)
+ },
}
return render(request, "requests/trainingrequests_merge.html", context)
diff --git a/amy/static/css/amy.css b/amy/static/css/amy.css
index a218d8561..562834d96 100644
--- a/amy/static/css/amy.css
+++ b/amy/static/css/amy.css
@@ -106,7 +106,6 @@ table .table-row-distinctive {
line-height: inherit;
overflow: visible;
text-transform: none;
- -webkit-appearance: button;
color: #dc3545;
background-color: transparent;
display: inline-block;
diff --git a/amy/templates/includes/trainingrequest_details.html b/amy/templates/includes/trainingrequest_details.html
index ae392d6d9..627e8608d 100644
--- a/amy/templates/includes/trainingrequest_details.html
+++ b/amy/templates/includes/trainingrequest_details.html
@@ -141,7 +141,7 @@
—
{% endif %}
- Data privacy agreement: |
+
---|
(Deprecated) Data privacy agreement: |
{{ object.data_privacy_agreement|yesno }} |
Code of Conduct agreement: |
{{ object.code_of_conduct_agreement|yesno }} |
@@ -149,5 +149,17 @@
{{ object.training_completion_agreement|yesno }} |
Teach a workshop within 12 months agreement: |
{{ object.workshop_teaching_agreement|yesno }} |
+
+ {{ consents_content.may_contact }} |
+ {% if consents.may_contact %}{{ consents.may_contact.term_option }}{% else %}Unset{% endif %} |
+
+
+ {{ consents_content.privacy_policy }} |
+ {% if consents.privacy_policy %}{{ consents.privacy_policy.term_option }}{% else %}Unset{% endif %} |
+
+
+ {{ consents_content.public_profile }} |
+ {% if consents.public_profile %}{{ consents.public_profile.term_option }}{% else %}Unset{% endif %} |
+
diff --git a/amy/templates/requests/trainingrequest.html b/amy/templates/requests/trainingrequest.html
index d5bbb72d0..9bebb70aa 100644
--- a/amy/templates/requests/trainingrequest.html
+++ b/amy/templates/requests/trainingrequest.html
@@ -27,7 +27,7 @@ Request details
Edit
-{% include "includes/trainingrequest_details.html" with admin=True object=req %}
+{% include "includes/trainingrequest_details.html" with admin=True object=req consents=consents %}
{% include "includes/comments.html" with object=req %}
diff --git a/amy/templates/requests/trainingrequests_merge.html b/amy/templates/requests/trainingrequests_merge.html
index bd5547a44..c00c488fa 100644
--- a/amy/templates/requests/trainingrequests_merge.html
+++ b/amy/templates/requests/trainingrequests_merge.html
@@ -197,8 +197,8 @@
Additional comments by submitter |
- {% if obj_a.comment %}{{ obj_a.comment }} {% else %}—{% endif %} | {% if obj_b.comment %}{{ obj_b.comment }} {% else %}—{% endif %} |
- {% include "includes/merge_radio.html" with field=form.comment %} |
+ {% if obj_a.user_notes %}{{ obj_a.user_notes }} {% else %}—{% endif %} | {% if obj_b.user_notes %}{{ obj_b.user_notes }} {% else %}—{% endif %} |
+ {% include "includes/merge_radio.html" with field=form.user_notes %} |
Training completion agreement? |
@@ -211,7 +211,7 @@
{% include "includes/merge_radio.html" with field=form.workshop_teaching_agreement %} |
- Data privacy agreement? |
+ Deprecated old-style consent Data privacy agreement? |
{{ obj_a.data_privacy_agreement|yesno }} | {{ obj_b.data_privacy_agreement|yesno }} |
{% include "includes/merge_radio.html" with field=form.data_privacy_agreement %} |
@@ -231,6 +231,26 @@
{% render_comment_list for obj_b %} |
{% include "includes/merge_radio.html" with field=form.comments %} |
+
+ New-style consents |
+
+
+ {% for consent in obj_a_consents.values %}
+ - {{ consent.term.content}}
+ - {{ consent.term_option }}
+ {% endfor %}
+
+ |
+
+
+ {% for consent in obj_b_consents.values %}
+ - {{ consent.term.content}}
+ - {{ consent.term_option }}
+ {% endfor %}
+
+ |
+ {% include "includes/merge_radio.html" with field=form.trainingrequestconsent_set %} |
+
diff --git a/amy/workshops/management/commands/create_superuser.py b/amy/workshops/management/commands/create_superuser.py
index fddce6e3f..51c8dc177 100644
--- a/amy/workshops/management/commands/create_superuser.py
+++ b/amy/workshops/management/commands/create_superuser.py
@@ -1,8 +1,12 @@
+import logging
+
from django.core.management.base import BaseCommand, CommandError
from communityroles.models import CommunityRole, CommunityRoleConfig
from workshops.models import Person
+logger = logging.getLogger("amy")
+
class Command(BaseCommand):
args = "no arguments"
@@ -12,7 +16,7 @@ def handle(self, *args, **options):
username = "admin"
if Person.objects.filter(username=username).exists():
- print("Admin user exists, quitting.")
+ logger.info("Admin user exists, quitting.")
return
try:
@@ -23,14 +27,14 @@ def handle(self, *args, **options):
email="admin@example.org",
password="admin",
)
- print("Created admin user")
+ logger.info("Created admin user")
role_config = CommunityRoleConfig.objects.get(name="instructor")
CommunityRole.objects.create(
config=role_config,
person=admin,
)
- print("Assigned Instructor community role to admin user")
+ logger.info("Assigned Instructor community role to admin user")
except Exception as e:
raise CommandError("Failed to create admin: {0}".format(str(e)))