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)))