From bdc0dc695ae23b11f3399e9d0196db0cec061eeb Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Sun, 20 Aug 2023 21:13:55 +0200 Subject: [PATCH 01/30] Use ModelForms and ModelFormSets to support bidning form with model instance for editing --- apps/accounts/forms.py | 70 ++++++++++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 620d180f..e2864881 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -1,20 +1,19 @@ import datetime -from betterforms.multiform import MultiModelForm -from convenient_formsets import ConvenientBaseFormSet from dal_select2_taggit import widgets as dal_widgets from django import forms from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth import get_user_model from django.contrib.auth.forms import UserChangeForm, UsernameField -from django.core.exceptions import NON_FIELD_ERRORS, ValidationError +from django.core.exceptions import ValidationError +from django.db.models.query import QuerySet from django.forms.formsets import BaseFormSet from django.forms.utils import ErrorList from django.utils.safestring import mark_safe from django_countries.fields import CountryField from dal_select2_taggit import widgets as dal_widgets from betterforms.multiform import MultiModelForm -from convenient_formsets import ConvenientBaseFormSet +from convenient_formsets import ConvenientBaseModelFormSet from file_resubmit.widgets import ResubmitFileWidget from taggit_labels.widgets import LabelWidget @@ -196,7 +195,7 @@ class Meta: fields = "__all__" -class OrgDetailsForm(forms.Form): +class OrgDetailsForm(forms.ModelForm): """ Part of multi-step registration form (screen 1) """ @@ -261,6 +260,10 @@ def save(self, commit=True) -> ProviderRequest: return pr + class Meta: + model = ac_models.ProviderRequest + fields = ["name", "website", "description", "authorised_by_org"] + class ServicesForm(forms.Form): """ @@ -302,7 +305,7 @@ class Meta: widgets = {"file": ResubmitFileWidget} -class MoreConvenientFormset(ConvenientBaseFormSet): +class MoreConvenientFormset(ConvenientBaseModelFormSet): def clean(self): """ ConvenientBaseFormset validates empty forms in a quirky way: @@ -334,8 +337,9 @@ def clean(self): # Uses ConvenientBaseFormSet to display add/delete buttons # and manage the forms inside the formset dynamically. class GreenEvidenceForm( - forms.formset_factory( - CredentialForm, + forms.modelformset_factory( + model=ac_models.ProviderRequestEvidence, + form=CredentialForm, extra=0, formset=MoreConvenientFormset, validate_min=True, @@ -370,10 +374,18 @@ class Meta: exclude = ["request"] -IpRangeFormset = forms.formset_factory( - IpRangeForm, formset=MoreConvenientFormset, extra=0 +IpRangeFormset = forms.modelformset_factory( + model=ac_models.ProviderRequestIPRange, + form=IpRangeForm, + formset=MoreConvenientFormset, + extra=0, +) +AsnFormset = forms.modelformset_factory( + model=ac_models.ProviderRequestASN, + form=AsnForm, + formset=MoreConvenientFormset, + extra=0, ) -AsnFormset = forms.formset_factory(AsnForm, formset=MoreConvenientFormset, extra=0) class ExtraNetworkInfoForm(forms.ModelForm): @@ -404,7 +416,27 @@ class Meta: fields = ["missing_network_explanation", "network_import_required"] -class NetworkFootprintForm(MultiModelForm): +class BetterMultiModelForm(MultiModelForm): + """ + MultiModelForm does not support injecting "instance" paramater + as querysets when child forms are ModelFormSets. + See more details: https://github.com/fusionbox/django-betterforms/issues/48 + + This helper class fixes that. + """ + + def get_form_args_kwargs(self, key, args, kwargs): + fargs, fkwargs = super().get_form_args_kwargs(key, args, kwargs) + try: + if isinstance(self.instances[key], QuerySet): + fkwargs["queryset"] = self.instances[key] + fkwargs.pop("instance") + except KeyError: + pass + return fargs, fkwargs + + +class NetworkFootprintForm(BetterMultiModelForm): """ Part of multi-step registration form (screen 4). @@ -540,8 +572,13 @@ class Meta: # Part of multi-step registration form (screen 2). # Uses ConvenientBaseFormSet to display add/delete buttons # and manage the forms inside the formset dynamically. -LocationsFormSet = forms.formset_factory( - LocationForm, extra=0, formset=MoreConvenientFormset, validate_min=True, min_num=1 +LocationsFormSet = forms.modelformset_factory( + model=ac_models.ProviderRequestLocation, + form=LocationForm, + extra=0, + formset=MoreConvenientFormset, + validate_min=True, + min_num=1, ) @@ -566,11 +603,12 @@ class Meta: exclude = ["request"] -class LocationStepForm(MultiModelForm): +class LocationStepForm(BetterMultiModelForm): """ A form to support at least one location as well as allowing a provider to flag up that they have a - significant number of locations to import + significant number of locations to import. + """ # We have to set base_fields to a dictionary because From 8bf3621b1c5524c0767c7818bd0a843b2a2b56fe Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Sun, 20 Aug 2023 21:14:43 +0200 Subject: [PATCH 02/30] Add wizard view on /requests/{id}/edit --- apps/accounts/urls.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py index 9e2bec8c..9b31c4c9 100644 --- a/apps/accounts/urls.py +++ b/apps/accounts/urls.py @@ -12,7 +12,7 @@ UserUpdateView, ProviderPortalHomeView, ProviderRequestDetailView, - ProviderRegistrationView, + ProviderRequestWizardView, ProviderAutocompleteView, ) @@ -115,9 +115,14 @@ ProviderRequestDetailView.as_view(), name="provider_request_detail", ), + path( + "requests//edit/", + ProviderRequestWizardView.as_view(ProviderRequestWizardView.FORMS), + name="provider_request_edit", + ), path( "requests/new/", - ProviderRegistrationView.as_view(ProviderRegistrationView.FORMS), + ProviderRequestWizardView.as_view(ProviderRequestWizardView.FORMS), name="provider_registration", ), path( @@ -125,7 +130,7 @@ ProviderAutocompleteView.as_view(), name="provider-autocomplete", ), - path( + path( "before-starting/", TemplateView.as_view(template_name="provider_portal/before_starting.html"), name="before-starting", From 02cbb61a16cc4ca764f647d67ae2d11aa87bc66e Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Sun, 20 Aug 2023 21:15:31 +0200 Subject: [PATCH 03/30] Use instances dictionary for editing form --- apps/accounts/views.py | 53 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index c1effc40..2c857b2a 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -222,7 +222,7 @@ def get_queryset(self) -> "QuerySet[ProviderRequest]": return ProviderRequest.objects.filter(created_by=self.request.user) -class ProviderRegistrationView(LoginRequiredMixin, WaffleFlagMixin, SessionWizardView): +class ProviderRequestWizardView(LoginRequiredMixin, WaffleFlagMixin, SessionWizardView): """ Multi-step registration for providers. - uses `django-formtools` SessionWizardView to display @@ -286,7 +286,7 @@ def done(self, form_list, form_dict, **kwargs): Reference: https://django-formtools.readthedocs.io/en/latest/wizard.html#formtools.wizard.views.WizardView.done """ # noqa - steps = ProviderRegistrationView.Steps + steps = ProviderRequestWizardView.Steps # process ORG_DETAILS form: extract ProviderRequest and Location org_details_form = form_dict[steps.ORG_DETAILS.value] @@ -410,6 +410,55 @@ def get_context_data(self, form, **kwargs): context["preview_forms"] = self._get_data_for_preview() return context + def get_form_kwargs(self, step=None): + """ + Workaround for injecting "instance" argument to MultiModelForm + """ + if step == self.Steps.LOCATIONS.value: + return {"instance": self.get_form_instance(step)} + return {} + + def get_instance_dict(self, request_id): + """ + Based on request_id, return existing instances of ProviderRequest + and related objects in a map that matches the structure of the forms. + """ + try: + pr_instance = ProviderRequest.objects.get(id=request_id) + except ProviderRequest.DoesNotExist: + return {} + + location_qs = pr_instance.providerrequestlocation_set.all() + evidence_qs = pr_instance.providerrequestevidence_set.all() + asn_qs = pr_instance.providerrequestasn_set.all() + ip_qs = pr_instance.providerrequestiprange_set.all() + consent = pr_instance.providerrequestconsent_set.get() + + # TODO: check behaviour: None vs {} vs no value + # TODO: check FormSet argument: iterable or queryset? + instance_dict = { + self.Steps.ORG_DETAILS.value: pr_instance, + self.Steps.LOCATIONS.value: { + "locations": location_qs, + }, + # self.Steps.SERVICES.value: None, + self.Steps.GREEN_EVIDENCE.value: evidence_qs, + self.Steps.NETWORK_FOOTPRINT.value: { + "ips": ip_qs, + "asns": asn_qs, + "extra": pr_instance, + }, + self.Steps.CONSENT.value: consent, + # self.Steps.PREVIEW.value: None, + } + return instance_dict + + def get_form_instance(self, step): + request_id = self.kwargs.get("request_id") + if not request_id: + return None + return self.get_instance_dict(request_id)[step] + def _send_notification_email(self, provider_request: ProviderRequest): """ Send notification to support staff, and the user to acknowledge their submission. From 64c59c7d01c4b27ea8316153d96ef409b472c2b9 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Sun, 20 Aug 2023 21:15:49 +0200 Subject: [PATCH 04/30] Fix tests after renaming wizard view --- apps/accounts/tests/test_provider_request.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/accounts/tests/test_provider_request.py b/apps/accounts/tests/test_provider_request.py index f5f65e0e..bf6bd860 100644 --- a/apps/accounts/tests/test_provider_request.py +++ b/apps/accounts/tests/test_provider_request.py @@ -36,7 +36,7 @@ def wizard_form_org_details_data(): as expected by the POST request. """ return { - "provider_registration_view-current_step": "0", + "provider_request_wizard_view-current_step": "0", "0-name": " ".join(faker.words(5)), "0-website": faker.url(), "0-description": faker.sentence(10), @@ -51,7 +51,7 @@ def wizard_form_org_location_data(): as expected by the POST request. """ return { - "provider_registration_view-current_step": "1", + "provider_request_wizard_view-current_step": "1", "locations__1-TOTAL_FORMS": "1", "locations__1-INITIAL_FORMS": "0", "locations__1-0-country": faker.country_code(), @@ -73,7 +73,7 @@ def wizard_form_services_data(): services_sample = random.sample([tag.slug for tag in tags_choices], 3) return { - "provider_registration_view-current_step": "2", + "provider_request_wizard_view-current_step": "2", "2-services": services_sample, } @@ -85,7 +85,7 @@ def wizard_form_evidence_data(fake_evidence): as expected by the POST request. """ return { - "provider_registration_view-current_step": "3", + "provider_request_wizard_view-current_step": "3", "3-TOTAL_FORMS": 2, "3-INITIAL_FORMS": 0, "3-0-title": " ".join(faker.words(3)), @@ -116,7 +116,7 @@ def wizard_form_network_data(sorted_ips): as expected by the POST request. """ return { - "provider_registration_view-current_step": "4", + "provider_request_wizard_view-current_step": "4", "ips__4-TOTAL_FORMS": "2", "ips__4-INITIAL_FORMS": "0", "ips__4-0-start": sorted_ips[0], @@ -136,7 +136,7 @@ def wizard_form_network_explanation_only(): form wizard, without any IP or AS information. """ return { - "provider_registration_view-current_step": "4", + "provider_request_wizard_view-current_step": "4", "ips__4-TOTAL_FORMS": "0", "ips__4-INITIAL_FORMS": "0", "asns__4-TOTAL_FORMS": "0", @@ -152,7 +152,7 @@ def wizard_form_consent(): as expected by the POST request. """ return { - "provider_registration_view-current_step": "5", + "provider_request_wizard_view-current_step": "5", "5-data_processing_opt_in": "on", "5-newsletter_opt_in": "off", } @@ -165,7 +165,7 @@ def wizard_form_preview(): as expected by the POST request. """ return { - "provider_registration_view-current_step": "6", + "provider_request_wizard_view-current_step": "6", } @@ -323,6 +323,7 @@ def test_wizard_view_happy_path( client.force_login(user) # when: submitting form data for consecutive wizard steps + for step, data in enumerate(form_data, 1): response = client.post(urls.reverse("provider_registration"), data, follow=True) # then: for all steps except the last one, From 632ee411cc4ff7aa6e94550579d996ae380a8d9d Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 23 Aug 2023 16:10:04 +0200 Subject: [PATCH 05/30] Handle updating existing objects when saving ModelForms --- apps/accounts/forms.py | 28 ++++++++++++------------ apps/accounts/models/provider_request.py | 6 ++++- apps/accounts/views.py | 12 +++++----- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index e2864881..e7938367 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -246,20 +246,6 @@ class OrgDetailsForm(forms.ModelForm): coerce=lambda x: x == "True", ) - def save(self, commit=True) -> ProviderRequest: - """ - Returns model instances of: ProviderRequest - based on the validated data bound to this Form - """ - pr = ProviderRequest.from_kwargs( - **self.cleaned_data, status=ProviderRequestStatus.PENDING_REVIEW.value - ) - - if commit: - pr.save() - - return pr - class Meta: model = ac_models.ProviderRequest fields = ["name", "website", "description", "authorised_by_org"] @@ -306,6 +292,20 @@ class Meta: class MoreConvenientFormset(ConvenientBaseModelFormSet): + def get_queryset(self): + """ + Built-in BaseModelFormSet uses model's default manager get_queryset, + which returns all objects. As a consequence + the formset would display all available objects. + + We change that behavior so that unless a "queryset" + parameter is passed to the formset (i.e. in editing mode), + empty queryset should be returned. + """ + if self.queryset is None: + return self.model.objects.none() + return super().get_queryset() + def clean(self): """ ConvenientBaseFormset validates empty forms in a quirky way: diff --git a/apps/accounts/models/provider_request.py b/apps/accounts/models/provider_request.py index d07f51a4..cfddd309 100644 --- a/apps/accounts/models/provider_request.py +++ b/apps/accounts/models/provider_request.py @@ -73,7 +73,11 @@ class ProviderRequest(TimeStampedModel): name = models.CharField(max_length=255) website = models.CharField(max_length=255) description = models.TextField() - status = models.CharField(choices=ProviderRequestStatus.choices, max_length=255) + status = models.CharField( + choices=ProviderRequestStatus.choices, + max_length=255, + default=ProviderRequestStatus.PENDING_REVIEW.value, + ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True ) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 2c857b2a..2c1c7e0b 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -291,6 +291,7 @@ def done(self, form_list, form_dict, **kwargs): # process ORG_DETAILS form: extract ProviderRequest and Location org_details_form = form_dict[steps.ORG_DETAILS.value] pr = org_details_form.save(commit=False) + pr.save() # process LOCATIONS form: extract locations locations_formset = form_dict[steps.LOCATIONS.value].forms["locations"] @@ -305,8 +306,7 @@ def done(self, form_list, form_dict, **kwargs): "location_import_required" ] - if location_import_required: - pr.location_import_required = location_import_required + pr.location_import_required = bool(location_import_required) # process SERVICES form: assign services to ProviderRequest services_form = form_dict[steps.SERVICES.value] @@ -413,8 +413,9 @@ def get_context_data(self, form, **kwargs): def get_form_kwargs(self, step=None): """ Workaround for injecting "instance" argument to MultiModelForm + when the form is used for editing existing request """ - if step == self.Steps.LOCATIONS.value: + if step == self.Steps.LOCATIONS.value and self.kwargs.get("request_id"): return {"instance": self.get_form_instance(step)} return {} @@ -428,20 +429,18 @@ def get_instance_dict(self, request_id): except ProviderRequest.DoesNotExist: return {} + # TODO: handle DoesNotExist location_qs = pr_instance.providerrequestlocation_set.all() evidence_qs = pr_instance.providerrequestevidence_set.all() asn_qs = pr_instance.providerrequestasn_set.all() ip_qs = pr_instance.providerrequestiprange_set.all() consent = pr_instance.providerrequestconsent_set.get() - # TODO: check behaviour: None vs {} vs no value - # TODO: check FormSet argument: iterable or queryset? instance_dict = { self.Steps.ORG_DETAILS.value: pr_instance, self.Steps.LOCATIONS.value: { "locations": location_qs, }, - # self.Steps.SERVICES.value: None, self.Steps.GREEN_EVIDENCE.value: evidence_qs, self.Steps.NETWORK_FOOTPRINT.value: { "ips": ip_qs, @@ -449,7 +448,6 @@ def get_instance_dict(self, request_id): "extra": pr_instance, }, self.Steps.CONSENT.value: consent, - # self.Steps.PREVIEW.value: None, } return instance_dict From 288c922895087123b71ab7e51eec1bb6b05a4e52 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 23 Aug 2023 17:23:24 +0200 Subject: [PATCH 06/30] Fix Network footprint form: loading initial data, finding duplicate entries --- apps/accounts/forms.py | 11 ++++++++--- apps/accounts/views.py | 6 +++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index e7938367..c78816bb 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -318,19 +318,24 @@ def clean(self): seen = [] for form in self.forms: - if not bool(form.cleaned_data): + # we strip the cleaned_data dict from "id" so that finding duplicated values + # works for both: new and existing entries + form_data = { + key: value for key, value in form.cleaned_data.items() if key != "id" + } + if not bool(form_data): e = ValidationError( "This row has no information - please complete or delete it", code="empty", ) form.add_error(None, e) - if form.cleaned_data in seen: + if form_data in seen: e = ValidationError( "Found a duplicated entry in the form, please remove the duplicate", code="duplicate", ) form.add_error(None, e) - seen.append(form.cleaned_data) + seen.append(form_data) # Part of multi-step registration form (screen 3). diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 2c1c7e0b..0e04ef26 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -415,7 +415,11 @@ def get_form_kwargs(self, step=None): Workaround for injecting "instance" argument to MultiModelForm when the form is used for editing existing request """ - if step == self.Steps.LOCATIONS.value and self.kwargs.get("request_id"): + affected_steps = [ + self.Steps.LOCATIONS.value, + self.Steps.NETWORK_FOOTPRINT.value, + ] + if self.kwargs.get("request_id") and step in affected_steps: return {"instance": self.get_form_instance(step)} return {} From 1da4c1383ae79b202632ff2c3a0df423ad000763 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 23 Aug 2023 18:52:09 +0200 Subject: [PATCH 07/30] Display initial values for bulk import and services --- apps/accounts/forms.py | 22 +++++++++++++++++++--- apps/accounts/models/provider_request.py | 8 -------- apps/accounts/views.py | 15 +++++++++++---- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index c78816bb..2f587cc7 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -251,7 +251,7 @@ class Meta: fields = ["name", "website", "description", "authorised_by_org"] -class ServicesForm(forms.Form): +class ServicesForm(forms.ModelForm): """ Part of multi-step registration form (screen 2) """ @@ -265,6 +265,21 @@ class ServicesForm(forms.Form): ), ) + def __init__(self, *args, **kwargs): + """ + Implement injecting initial values for services field (for editing existing objects). + By default the initial value is passed as a queryset, + but TaggableManager does not handle that well - we pass a list instead. + """ + super().__init__(*args, **kwargs) + instance = kwargs.get("instance") + if instance: + self.initial = {"services": [s for s in instance.services.slugs()]} + + class Meta: + model = ac_models.ProviderRequest + fields = ["services"] + class CredentialForm(forms.ModelForm): class Meta: @@ -587,7 +602,7 @@ class Meta: ) -class LocationExtraForm(forms.Form): +class LocationExtraForm(forms.ModelForm): """ A form for information relating to the location step, not a to a single one of the locations listed on the location step. @@ -605,7 +620,8 @@ class LocationExtraForm(forms.Form): ) class Meta: - exclude = ["request"] + model = ac_models.ProviderRequest + fields = ["location_import_required"] class LocationStepForm(BetterMultiModelForm): diff --git a/apps/accounts/models/provider_request.py b/apps/accounts/models/provider_request.py index cfddd309..9642411f 100644 --- a/apps/accounts/models/provider_request.py +++ b/apps/accounts/models/provider_request.py @@ -127,14 +127,6 @@ def from_kwargs(**kwargs) -> "ProviderRequest": pr_data.setdefault("status", ProviderRequestStatus.OPEN.value) return ProviderRequest.objects.create(**pr_data) - def set_services_from_slugs(self, service_slugs: Iterable[str]) -> None: - """ - Given list of service slugs (corresponding to Tag slugs) - apply matching services to the ProviderRequest object - """ - services = Service.objects.filter(slug__in=service_slugs) - self.services.set(services) - @classmethod def get_service_choices(cls) -> List[Tuple[int, str]]: """ diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 0e04ef26..fc08e904 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -310,7 +310,8 @@ def done(self, form_list, form_dict, **kwargs): # process SERVICES form: assign services to ProviderRequest services_form = form_dict[steps.SERVICES.value] - pr.set_services_from_slugs(services_form.cleaned_data["services"]) + services = services_form.cleaned_data["services"] + pr.services.set(*services) pr.created_by = self.request.user pr.save() @@ -341,9 +342,12 @@ def done(self, form_list, form_dict, **kwargs): network_explanation = extra_network_form.cleaned_data.get( "missing_network_explanation" ) - if network_explanation: - pr.missing_network_explanation = network_explanation - pr.save() + network_import_required = extra_network_form.cleaned_data.get( + "network_import_required" + ) + pr.missing_network_explanation = bool(network_explanation) + pr.network_import_required = bool(network_import_required) + pr.save() # process CONSENT form consent_form = form_dict[steps.CONSENT.value] @@ -444,7 +448,9 @@ def get_instance_dict(self, request_id): self.Steps.ORG_DETAILS.value: pr_instance, self.Steps.LOCATIONS.value: { "locations": location_qs, + "extra": pr_instance, }, + self.Steps.SERVICES.value: pr_instance, self.Steps.GREEN_EVIDENCE.value: evidence_qs, self.Steps.NETWORK_FOOTPRINT.value: { "ips": ip_qs, @@ -456,6 +462,7 @@ def get_instance_dict(self, request_id): return instance_dict def get_form_instance(self, step): + # TODO: optimize this - do not construct instance_dict on every call request_id = self.kwargs.get("request_id") if not request_id: return None From ec6536bf715ac5df2ac9286be9a046a8986806c4 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Thu, 24 Aug 2023 17:42:27 +0200 Subject: [PATCH 08/30] Fix bugs found by tests --- apps/accounts/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index fc08e904..4224e536 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -311,7 +311,7 @@ def done(self, form_list, form_dict, **kwargs): # process SERVICES form: assign services to ProviderRequest services_form = form_dict[steps.SERVICES.value] services = services_form.cleaned_data["services"] - pr.services.set(*services) + pr.services.set(services) pr.created_by = self.request.user pr.save() @@ -345,7 +345,7 @@ def done(self, form_list, form_dict, **kwargs): network_import_required = extra_network_form.cleaned_data.get( "network_import_required" ) - pr.missing_network_explanation = bool(network_explanation) + pr.missing_network_explanation = network_explanation pr.network_import_required = bool(network_import_required) pr.save() From 158ca4df51952c60a965776bab2ae10dcbd18502 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Thu, 24 Aug 2023 18:28:41 +0200 Subject: [PATCH 09/30] Render 404 when accessing /{request_id}/edit unauthorized or for non-existing request --- apps/accounts/views.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 4224e536..dc1fbc76 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -8,7 +8,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.sites.shortcuts import get_current_site from django.db.models.query import QuerySet -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect, Http404 from django.urls import reverse from django.utils.safestring import mark_safe from django.utils.encoding import force_text @@ -272,6 +272,29 @@ class Steps(Enum): Steps.PREVIEW.value: "provider_registration/preview.html", } + def dispatch(self, request, *args, **kwargs): + """ + Overwrite method from TemplateView to decide for which users + to render a 404 page + """ + request_id = kwargs.get("request_id") + + # all users can access this view to create a new request + if not request_id: + return super().dispatch(request, *args, **kwargs) + + # edit view can be only accessed for existing PRs + try: + pr = ProviderRequest.objects.get(id=request_id) + except ProviderRequest.DoesNotExist: + raise Http404("Page not found") + + # only admins and creators can access for editing existing requests + if not request.user.is_admin and request.user.id != pr.created_by.id: + raise Http404("Page not found") + + return super().dispatch(request, *args, **kwargs) + def done(self, form_list, form_dict, **kwargs): """ This method is called when all the forms are validated and submitted. From 062aa932ef60dc1985db38bee2a3459d1a1ddf8e Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 30 Aug 2023 13:36:12 +0200 Subject: [PATCH 10/30] Add missing dev dependency --- Pipfile | 1 + Pipfile.lock | 576 +++++++++++++++++++++++++-------------------------- 2 files changed, 281 insertions(+), 296 deletions(-) diff --git a/Pipfile b/Pipfile index d5f93f57..09ffa752 100644 --- a/Pipfile +++ b/Pipfile @@ -30,6 +30,7 @@ sphinxcontrib-mermaid = "*" furo = "==2022.4.7" myst-parser = "==0.18.0" ansible-lint = "*" +django-browser-reload = "*" [packages] django = { extras = ["bcrypt"], version = "~=3.2" } diff --git a/Pipfile.lock b/Pipfile.lock index 2f8923ed..466523bd 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a610dca2501a2e4dda835a1e930d400725cc57ec29d4d4a5589b60f9568dd8e7" + "sha256": "291cca29d51fe464e188ff168a929001b92229b69522e167895f06a20adf17b2" }, "pipfile-spec": 6, "requires": {}, @@ -141,11 +141,11 @@ }, "awscli": { "hashes": [ - "sha256:211b6703d8dfb696ab3d8199a94a227819a62bed6337f3caab7339099a4475a8", - "sha256:dcbf7d67d1b89069bf287f422a7a62b7747773377f04de2bb4105c41977119e5" + "sha256:20868edf4ef0b7f368ef25668853f7f70d1a7412f057ebee022c0876647ea952", + "sha256:de1bdb6864018fe9db19293073538db82c6f469bbb471f088dc1fb331b2344e8" ], "index": "pypi", - "version": "==1.29.52" + "version": "==1.29.37" }, "awscli-plugin-endpoint": { "hashes": [ @@ -191,19 +191,19 @@ }, "boto3": { "hashes": [ - "sha256:1d36db102517d62c6968b3b0636303241f56859d12dd071def4882fc6e030b20", - "sha256:a34fc153cb2f6fb2f79a764286c967392e8aae9412381d943bddc576c4f7631a" + "sha256:4aec1b54ba6cd352abba2cdd7cdc76e631a4d3ce79c55c0719f85f9c9842e4a2", + "sha256:709cf438ad3ea48d426e4659538fe1148fc2719469b52179d07a11c5d26abac6" ], "index": "pypi", - "version": "==1.28.52" + "version": "==1.28.37" }, "botocore": { "hashes": [ - "sha256:46b0a75a38521aa6a75fddccb1542e002930e609d4e13516f40fef170d32e515", - "sha256:6d09881c5a8be34b497872ca3936f8757d886a6f42f2a8703411928189cfedc0" + "sha256:5c92c8bc3c6b49950c95501b30f0ac551fd4952359b53a6fba243094028157de", + "sha256:72e10759be3dff39c5eeb29f85c11a227c369c946d044f2caf62c352d6a6fc06" ], "markers": "python_version >= '3.7'", - "version": "==1.31.52" + "version": "==1.31.37" }, "brotli": { "hashes": [ @@ -1163,37 +1163,34 @@ }, "numpy": { "hashes": [ - "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f", - "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61", - "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7", - "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400", - "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef", - "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2", - "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d", - "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc", - "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835", - "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706", - "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5", - "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4", - "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6", - "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463", - "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a", - "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f", - "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e", - "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e", - "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694", - "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8", - "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64", - "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d", - "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc", - "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254", - "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2", - "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1", - "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810", - "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9" - ], - "index": "pypi", - "version": "==1.24.4" + "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2", + "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55", + "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf", + "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01", + "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca", + "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901", + "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d", + "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4", + "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf", + "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380", + "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044", + "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545", + "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f", + "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f", + "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3", + "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364", + "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9", + "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418", + "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f", + "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295", + "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3", + "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187", + "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926", + "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357", + "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760" + ], + "index": "pypi", + "version": "==1.25.2" }, "packaging": { "hashes": [ @@ -1421,11 +1418,11 @@ }, "rich": { "hashes": [ - "sha256:87b43e0543149efa1253f485cd845bb7ee54df16c9617b8a893650ab84b4acb6", - "sha256:9257b468badc3d347e146a4faa268ff229039d4c2d176ab0cffb4c4fbc73d5d9" + "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808", + "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39" ], "index": "pypi", - "version": "==13.5.3" + "version": "==13.5.2" }, "rsa": { "hashes": [ @@ -1496,11 +1493,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:64a7141005fb775b9db298a30de93e3b83e0ddd1232dc6f36eb38aebc1553291", - "sha256:6de2e88304873484207fed836388e422aeff000609b104c802749fd89d56ba5b" + "sha256:2e53ad63f96bb9da6570ba2e755c267e529edcf58580a2c0d2a11ef26e1e678b", + "sha256:7dc873b87e1faf4d00614afd1058bfa1522942f33daef8a59f90de8ed75cd10c" ], "index": "pypi", - "version": "==1.31.0" + "version": "==1.30.0" }, "six": { "hashes": [ @@ -1563,11 +1560,11 @@ }, "sqlite-utils": { "hashes": [ - "sha256:58da19f64b37fd47e33158ac4dadf2616701cd17d825a1625866d04647f72805", - "sha256:e0f03e6976b05bdb7a5c56454971a0e980fc16dbfd3512bbd3bdcac4f0e4370e" + "sha256:8f6fe7f8d12772cd5cf4594703a98dcd0c37c0fd6820dd20541ba74b9fca363a", + "sha256:d95591db18716ce7eac9a6359f4958598944d9e6640ccc42a2be6ab8456edddf" ], "index": "pypi", - "version": "==3.35.1" + "version": "==3.35" }, "sqlparse": { "hashes": [ @@ -1755,21 +1752,37 @@ "markers": "python_version >= '3.6'", "version": "==0.7.13" }, + "ansible-compat": { + "hashes": [ + "sha256:7560e511a660e286c4e8777d82e25990526e45601c37b872c1b64a62126239a5", + "sha256:f58135f5d123e08fdb7e11849b82945e1900f8c899ba0859d4b41b25c76ca955" + ], + "markers": "python_version >= '3.9'", + "version": "==4.1.8" + }, "ansible-core": { "hashes": [ - "sha256:51ce7c363e619e82e9b8cd3af72ffe75a9567a92687b9cc639dba0cfd30397f3", - "sha256:5923c586dbb92b3661aaa4d70a2ccc2216db21079c6f9b517f05f4f876934be4" + "sha256:261bc01a15274fc5a6950d5b92b9aa1b7d7c6e8f7543c914505e5bfd9744793a", + "sha256:bc2f5ab74e1c81609aaa9bc8f7f92d939d8e1c847923290301231bdf4dadc812" ], - "markers": "python_version < '3.9'", - "version": "==2.13.12" + "markers": "python_version >= '3.9'", + "version": "==2.15.3" }, "ansible-lint": { "hashes": [ - "sha256:419b1a2c8523fbe0c0e8a6c51a5c41a692badb5a530e880a9050ac3a69c3fde3", - "sha256:435c12b4fd88da815af6821f3bf8b04ebb651811da89a11c9d190baff21badaa" + "sha256:2c5dc2553604503cf46af91226f92394849833bf20bd4d3a170866e1c3a02779", + "sha256:54744ee7f8fd0ec38051f0b6df2153523939a391ee4bb48f0885b5fcdd82f9b9" ], "index": "pypi", - "version": "==6.13.1" + "version": "==6.18.0" + }, + "appnope": { + "hashes": [ + "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24", + "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e" + ], + "markers": "sys_platform == 'darwin'", + "version": "==0.1.3" }, "appnope": { "hashes": [ @@ -1825,28 +1838,6 @@ ], "version": "==0.2.0" }, - "backports.zoneinfo": { - "hashes": [ - "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf", - "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328", - "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546", - "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6", - "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570", - "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9", - "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7", - "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987", - "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722", - "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582", - "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc", - "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b", - "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1", - "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08", - "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac", - "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2" - ], - "markers": "python_version < '3.9'", - "version": "==0.2.1" - }, "beautifulsoup4": { "hashes": [ "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da", @@ -2068,90 +2059,90 @@ "coverage": { "extras": [], "hashes": [ - "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375", - "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344", - "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e", - "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745", - "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f", - "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194", - "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a", - "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f", - "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760", - "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8", - "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392", - "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d", - "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc", - "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40", - "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981", - "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0", - "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92", - "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3", - "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0", - "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086", - "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7", - "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465", - "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140", - "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952", - "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3", - "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8", - "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f", - "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593", - "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0", - "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204", - "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037", - "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276", - "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9", - "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26", - "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce", - "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7", - "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136", - "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a", - "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4", - "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c", - "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f", - "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832", - "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3", - "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969", - "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520", - "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887", - "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3", - "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6", - "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1", - "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff", - "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981", - "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e" - ], - "index": "pypi", - "version": "==7.3.1" + "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34", + "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e", + "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7", + "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b", + "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3", + "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985", + "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95", + "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2", + "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a", + "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74", + "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd", + "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af", + "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54", + "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865", + "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214", + "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54", + "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe", + "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0", + "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321", + "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446", + "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e", + "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527", + "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12", + "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f", + "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f", + "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84", + "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479", + "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e", + "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873", + "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70", + "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0", + "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977", + "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51", + "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28", + "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1", + "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254", + "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1", + "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd", + "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689", + "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d", + "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543", + "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9", + "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637", + "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071", + "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482", + "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1", + "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b", + "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5", + "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a", + "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393", + "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a", + "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba" + ], + "index": "pypi", + "version": "==7.3.0" }, "cryptography": { "hashes": [ - "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67", - "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311", - "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8", - "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13", - "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143", - "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f", - "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829", - "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd", - "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397", - "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac", - "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d", - "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a", - "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839", - "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e", - "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6", - "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9", - "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860", - "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca", - "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91", - "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d", - "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714", - "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb", - "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f" + "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306", + "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84", + "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47", + "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d", + "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116", + "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207", + "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81", + "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087", + "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd", + "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507", + "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858", + "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae", + "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34", + "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906", + "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd", + "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922", + "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7", + "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4", + "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574", + "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1", + "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c", + "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e", + "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de" ], "markers": "python_version >= '3.7'", - "version": "==41.0.4" + "version": "==41.0.3" }, "decorator": { "hashes": [ @@ -2180,6 +2171,14 @@ "index": "pypi", "version": "==3.2.21" }, + "django-browser-reload": { + "hashes": [ + "sha256:ce9576bd31562fda01dd8fca73e07696a9a1c7138d2aee084abfb7315dc633a4", + "sha256:fe232478a58e9e5d8b703537651d5a76b017d2208d1da5dfbe013937b5cca065" + ], + "index": "pypi", + "version": "==1.11.0" + }, "django-debug-toolbar": { "hashes": [ "sha256:af99128c06e8e794479e65ab62cc6c7d1e74e1c19beb44dcbf9bad7a9c017327", @@ -2229,19 +2228,19 @@ }, "faker": { "hashes": [ - "sha256:8fba91068dc26e3159c1ac9f22444a2338704b0991d86605322e454bda420092", - "sha256:d5d5953556b0fb428a46019e03fc2d40eab2980135ddef5a9eb3d054947fdf83" + "sha256:a6624d9574623bb27dfca33fff94581cd7b23b562901db8ad59acbde9a52543e", + "sha256:e2722fdf622cf24e974aaba15a3dee97a6f8b98d869bd827ff1af9c87695af46" ], "index": "pypi", - "version": "==19.6.2" + "version": "==19.3.1" }, "filelock": { "hashes": [ - "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4", - "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd" + "sha256:0ecc1dd2ec4672a10c8550a8182f1bd0c0a5088470ecd5a125e45f49472fac3d", + "sha256:f067e40ccc40f2b48395a80fcbd4728262fab54e232e090a4063ab804179efeb" ], "markers": "python_version >= '3.8'", - "version": "==3.12.4" + "version": "==3.12.3" }, "flake8": { "hashes": [ @@ -2261,11 +2260,11 @@ }, "hypothesis": { "hashes": [ - "sha256:e1d36522824d62bb3e9fcb7b57dd4a6ca330bb36921324bb19c476bdafabeda7", - "sha256:e5d75d70f5a4fc372cddf03ec6141237a0a270ed106aeb2156a4984f06d37b0f" + "sha256:06069ff2f18b530a253c0b853b9fae299369cf8f025b3ad3b86ee7131ecd3207", + "sha256:7950944b4a8b7610ab32d077a05e48bec30ecee7385e4d75eedd8120974b199e" ], "index": "pypi", - "version": "==6.86.2" + "version": "==6.82.7" }, "idna": { "hashes": [ @@ -2293,11 +2292,11 @@ }, "importlib-resources": { "hashes": [ - "sha256:9d48dcccc213325e810fd723e7fbb45ccb39f6cf5c31f00cf2b965f5f10f3cb9", - "sha256:aa50258bbfa56d4e33fbd8aa3ef48ded10d1735f11532b8df95388cc6bdb7e83" + "sha256:2238159eb743bd85304a16e0536048b3e991c531d1cd51c4a834d1ccf2829057", + "sha256:4df460394562b4581bb4e4087ad9447bd433148fba44241754ec3152499f1d1b" ], - "markers": "python_version < '3.9'", - "version": "==6.1.0" + "markers": "python_version < '3.10'", + "version": "==5.0.7" }, "inflection": { "hashes": [ @@ -2325,11 +2324,11 @@ }, "ipython": { "hashes": [ - "sha256:c7b80eb7f5a855a88efc971fda506ff7a91c280b42cdae26643e0f601ea281ea", - "sha256:ea8801f15dfe4ffb76dea1b09b847430ffd70d827b41735c64a0638a04103bfc" + "sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1", + "sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf" ], "index": "pypi", - "version": "==8.12.2" + "version": "==8.14.0" }, "isort": { "hashes": [ @@ -2357,11 +2356,11 @@ }, "jsonschema": { "hashes": [ - "sha256:cd5f1f9ed9444e554b38ba003af06c0a8c2868131e56bfbef0550fb450c0330e", - "sha256:ec84cc37cfa703ef7cd4928db24f9cb31428a5d0fa77747b8b51a847458e0bbf" + "sha256:043dc26a3845ff09d20e4420d6012a9c91c9aa8999fa184e7efcfeccb41e32cb", + "sha256:6e1e7569ac13be8139b2dd2c21a55d350066ee3f80df06c608b398cdc6f30e8f" ], "markers": "python_version >= '3.8'", - "version": "==4.19.1" + "version": "==4.19.0" }, "jsonschema-specifications": { "hashes": [ @@ -2581,14 +2580,6 @@ ], "version": "==0.7.5" }, - "pkgutil-resolve-name": { - "hashes": [ - "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174", - "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e" - ], - "markers": "python_version < '3.9'", - "version": "==1.3.10" - }, "platformdirs": { "hashes": [ "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d", @@ -2738,13 +2729,6 @@ "index": "pypi", "version": "==2.8.2" }, - "pytz": { - "hashes": [ - "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b", - "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7" - ], - "version": "==2023.3.post1" - }, "pyyaml": { "hashes": [ "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", @@ -2819,121 +2803,121 @@ }, "resolvelib": { "hashes": [ - "sha256:c6ea56732e9fb6fca1b2acc2ccc68a0b6b8c566d8f3e78e0443310ede61dbd37", - "sha256:d9b7907f055c3b3a2cfc56c914ffd940122915826ff5fb5b1de0c99778f4de98" + "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309", + "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf" ], - "version": "==0.8.1" + "version": "==1.0.1" }, "rich": { "hashes": [ - "sha256:87b43e0543149efa1253f485cd845bb7ee54df16c9617b8a893650ab84b4acb6", - "sha256:9257b468badc3d347e146a4faa268ff229039d4c2d176ab0cffb4c4fbc73d5d9" + "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808", + "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39" ], "index": "pypi", - "version": "==13.5.3" + "version": "==13.5.2" }, "rpds-py": { "hashes": [ - "sha256:015de2ce2af1586ff5dc873e804434185199a15f7d96920ce67e50604592cae9", - "sha256:061c3ff1f51ecec256e916cf71cc01f9975af8fb3af9b94d3c0cc8702cfea637", - "sha256:08a80cf4884920863623a9ee9a285ee04cef57ebedc1cc87b3e3e0f24c8acfe5", - "sha256:09362f86ec201288d5687d1dc476b07bf39c08478cde837cb710b302864e7ec9", - "sha256:0bb4f48bd0dd18eebe826395e6a48b7331291078a879295bae4e5d053be50d4c", - "sha256:106af1653007cc569d5fbb5f08c6648a49fe4de74c2df814e234e282ebc06957", - "sha256:11fdd1192240dda8d6c5d18a06146e9045cb7e3ba7c06de6973000ff035df7c6", - "sha256:16a472300bc6c83fe4c2072cc22b3972f90d718d56f241adabc7ae509f53f154", - "sha256:176287bb998fd1e9846a9b666e240e58f8d3373e3bf87e7642f15af5405187b8", - "sha256:177914f81f66c86c012311f8c7f46887ec375cfcfd2a2f28233a3053ac93a569", - "sha256:177c9dd834cdf4dc39c27436ade6fdf9fe81484758885f2d616d5d03c0a83bd2", - "sha256:187700668c018a7e76e89424b7c1042f317c8df9161f00c0c903c82b0a8cac5c", - "sha256:1d9b5ee46dcb498fa3e46d4dfabcb531e1f2e76b477e0d99ef114f17bbd38453", - "sha256:22da15b902f9f8e267020d1c8bcfc4831ca646fecb60254f7bc71763569f56b1", - "sha256:24cd91a03543a0f8d09cb18d1cb27df80a84b5553d2bd94cba5979ef6af5c6e7", - "sha256:255f1a10ae39b52122cce26ce0781f7a616f502feecce9e616976f6a87992d6b", - "sha256:271c360fdc464fe6a75f13ea0c08ddf71a321f4c55fc20a3fe62ea3ef09df7d9", - "sha256:2ed83d53a8c5902ec48b90b2ac045e28e1698c0bea9441af9409fc844dc79496", - "sha256:2f3e1867dd574014253b4b8f01ba443b9c914e61d45f3674e452a915d6e929a3", - "sha256:35fbd23c1c8732cde7a94abe7fb071ec173c2f58c0bd0d7e5b669fdfc80a2c7b", - "sha256:37d0c59548ae56fae01c14998918d04ee0d5d3277363c10208eef8c4e2b68ed6", - "sha256:39d05e65f23a0fe897b6ac395f2a8d48c56ac0f583f5d663e0afec1da89b95da", - "sha256:3ad59efe24a4d54c2742929001f2d02803aafc15d6d781c21379e3f7f66ec842", - "sha256:3aed39db2f0ace76faa94f465d4234aac72e2f32b009f15da6492a561b3bbebd", - "sha256:3bbac1953c17252f9cc675bb19372444aadf0179b5df575ac4b56faaec9f6294", - "sha256:40bc802a696887b14c002edd43c18082cb7b6f9ee8b838239b03b56574d97f71", - "sha256:42f712b4668831c0cd85e0a5b5a308700fe068e37dcd24c0062904c4e372b093", - "sha256:448a66b8266de0b581246ca7cd6a73b8d98d15100fb7165974535fa3b577340e", - "sha256:485301ee56ce87a51ccb182a4b180d852c5cb2b3cb3a82f7d4714b4141119d8c", - "sha256:485747ee62da83366a44fbba963c5fe017860ad408ccd6cd99aa66ea80d32b2e", - "sha256:4cf0855a842c5b5c391dd32ca273b09e86abf8367572073bd1edfc52bc44446b", - "sha256:4eca20917a06d2fca7628ef3c8b94a8c358f6b43f1a621c9815243462dcccf97", - "sha256:4ed172d0c79f156c1b954e99c03bc2e3033c17efce8dd1a7c781bc4d5793dfac", - "sha256:5267cfda873ad62591b9332fd9472d2409f7cf02a34a9c9cb367e2c0255994bf", - "sha256:52b5cbc0469328e58180021138207e6ec91d7ca2e037d3549cc9e34e2187330a", - "sha256:53d7a3cd46cdc1689296348cb05ffd4f4280035770aee0c8ead3bbd4d6529acc", - "sha256:563646d74a4b4456d0cf3b714ca522e725243c603e8254ad85c3b59b7c0c4bf0", - "sha256:570cc326e78ff23dec7f41487aa9c3dffd02e5ee9ab43a8f6ccc3df8f9327623", - "sha256:5aca759ada6b1967fcfd4336dcf460d02a8a23e6abe06e90ea7881e5c22c4de6", - "sha256:5de11c041486681ce854c814844f4ce3282b6ea1656faae19208ebe09d31c5b8", - "sha256:5e271dd97c7bb8eefda5cca38cd0b0373a1fea50f71e8071376b46968582af9b", - "sha256:642ed0a209ced4be3a46f8cb094f2d76f1f479e2a1ceca6de6346a096cd3409d", - "sha256:6446002739ca29249f0beaaf067fcbc2b5aab4bc7ee8fb941bd194947ce19aff", - "sha256:691d50c99a937709ac4c4cd570d959a006bd6a6d970a484c84cc99543d4a5bbb", - "sha256:69b857a7d8bd4f5d6e0db4086da8c46309a26e8cefdfc778c0c5cc17d4b11e08", - "sha256:6ac3fefb0d168c7c6cab24fdfc80ec62cd2b4dfd9e65b84bdceb1cb01d385c33", - "sha256:6c9141af27a4e5819d74d67d227d5047a20fa3c7d4d9df43037a955b4c748ec5", - "sha256:7170cbde4070dc3c77dec82abf86f3b210633d4f89550fa0ad2d4b549a05572a", - "sha256:763ad59e105fca09705d9f9b29ecffb95ecdc3b0363be3bb56081b2c6de7977a", - "sha256:77076bdc8776a2b029e1e6ffbe6d7056e35f56f5e80d9dc0bad26ad4a024a762", - "sha256:7cd020b1fb41e3ab7716d4d2c3972d4588fdfbab9bfbbb64acc7078eccef8860", - "sha256:821392559d37759caa67d622d0d2994c7a3f2fb29274948ac799d496d92bca73", - "sha256:829e91f3a8574888b73e7a3feb3b1af698e717513597e23136ff4eba0bc8387a", - "sha256:850c272e0e0d1a5c5d73b1b7871b0a7c2446b304cec55ccdb3eaac0d792bb065", - "sha256:87d9b206b1bd7a0523375dc2020a6ce88bca5330682ae2fe25e86fd5d45cea9c", - "sha256:8bd01ff4032abaed03f2db702fa9a61078bee37add0bd884a6190b05e63b028c", - "sha256:8d54bbdf5d56e2c8cf81a1857250f3ea132de77af543d0ba5dce667183b61fec", - "sha256:8efaeb08ede95066da3a3e3c420fcc0a21693fcd0c4396d0585b019613d28515", - "sha256:8f94fdd756ba1f79f988855d948ae0bad9ddf44df296770d9a58c774cfbcca72", - "sha256:95cde244e7195b2c07ec9b73fa4c5026d4a27233451485caa1cd0c1b55f26dbd", - "sha256:975382d9aa90dc59253d6a83a5ca72e07f4ada3ae3d6c0575ced513db322b8ec", - "sha256:9dd9d9d9e898b9d30683bdd2b6c1849449158647d1049a125879cb397ee9cd12", - "sha256:a019a344312d0b1f429c00d49c3be62fa273d4a1094e1b224f403716b6d03be1", - "sha256:a4d9bfda3f84fc563868fe25ca160c8ff0e69bc4443c5647f960d59400ce6557", - "sha256:a657250807b6efd19b28f5922520ae002a54cb43c2401e6f3d0230c352564d25", - "sha256:a771417c9c06c56c9d53d11a5b084d1de75de82978e23c544270ab25e7c066ff", - "sha256:aad6ed9e70ddfb34d849b761fb243be58c735be6a9265b9060d6ddb77751e3e8", - "sha256:ae87137951bb3dc08c7d8bfb8988d8c119f3230731b08a71146e84aaa919a7a9", - "sha256:af247fd4f12cca4129c1b82090244ea5a9d5bb089e9a82feb5a2f7c6a9fe181d", - "sha256:b5d4bdd697195f3876d134101c40c7d06d46c6ab25159ed5cbd44105c715278a", - "sha256:b9255e7165083de7c1d605e818025e8860636348f34a79d84ec533546064f07e", - "sha256:c22211c165166de6683de8136229721f3d5c8606cc2c3d1562da9a3a5058049c", - "sha256:c55f9821f88e8bee4b7a72c82cfb5ecd22b6aad04033334f33c329b29bfa4da0", - "sha256:c7aed97f2e676561416c927b063802c8a6285e9b55e1b83213dfd99a8f4f9e48", - "sha256:cd2163f42868865597d89399a01aa33b7594ce8e2c4a28503127c81a2f17784e", - "sha256:ce5e7504db95b76fc89055c7f41e367eaadef5b1d059e27e1d6eabf2b55ca314", - "sha256:cff7351c251c7546407827b6a37bcef6416304fc54d12d44dbfecbb717064717", - "sha256:d27aa6bbc1f33be920bb7adbb95581452cdf23005d5611b29a12bb6a3468cc95", - "sha256:d3b52a67ac66a3a64a7e710ba629f62d1e26ca0504c29ee8cbd99b97df7079a8", - "sha256:de61e424062173b4f70eec07e12469edde7e17fa180019a2a0d75c13a5c5dc57", - "sha256:e10e6a1ed2b8661201e79dff5531f8ad4cdd83548a0f81c95cf79b3184b20c33", - "sha256:e1a0ffc39f51aa5f5c22114a8f1906b3c17eba68c5babb86c5f77d8b1bba14d1", - "sha256:e22491d25f97199fc3581ad8dd8ce198d8c8fdb8dae80dea3512e1ce6d5fa99f", - "sha256:e626b864725680cd3904414d72e7b0bd81c0e5b2b53a5b30b4273034253bb41f", - "sha256:e8c71ea77536149e36c4c784f6d420ffd20bea041e3ba21ed021cb40ce58e2c9", - "sha256:e8d0f0eca087630d58b8c662085529781fd5dc80f0a54eda42d5c9029f812599", - "sha256:ea65b59882d5fa8c74a23f8960db579e5e341534934f43f3b18ec1839b893e41", - "sha256:ea93163472db26ac6043e8f7f93a05d9b59e0505c760da2a3cd22c7dd7111391", - "sha256:eab75a8569a095f2ad470b342f2751d9902f7944704f0571c8af46bede438475", - "sha256:ed8313809571a5463fd7db43aaca68ecb43ca7a58f5b23b6e6c6c5d02bdc7882", - "sha256:ef5fddfb264e89c435be4adb3953cef5d2936fdeb4463b4161a6ba2f22e7b740", - "sha256:ef750a20de1b65657a1425f77c525b0183eac63fe7b8f5ac0dd16f3668d3e64f", - "sha256:efb9ece97e696bb56e31166a9dd7919f8f0c6b31967b454718c6509f29ef6fee", - "sha256:f4c179a7aeae10ddf44c6bac87938134c1379c49c884529f090f9bf05566c836", - "sha256:f602881d80ee4228a2355c68da6b296a296cd22bbb91e5418d54577bbf17fa7c", - "sha256:fc2200e79d75b5238c8d69f6a30f8284290c777039d331e7340b6c17cad24a5a", - "sha256:fcc1ebb7561a3e24a6588f7c6ded15d80aec22c66a070c757559b57b17ffd1cb" + "sha256:00215f6a9058fbf84f9d47536902558eb61f180a6b2a0fa35338d06ceb9a2e5a", + "sha256:0028eb0967942d0d2891eae700ae1a27b7fd18604cfcb16a1ef486a790fee99e", + "sha256:0155c33af0676fc38e1107679be882077680ad1abb6303956b97259c3177e85e", + "sha256:063411228b852fb2ed7485cf91f8e7d30893e69b0acb207ec349db04cccc8225", + "sha256:0700c2133ba203c4068aaecd6a59bda22e06a5e46255c9da23cbf68c6942215d", + "sha256:08e08ccf5b10badb7d0a5c84829b914c6e1e1f3a716fdb2bf294e2bd01562775", + "sha256:0d292cabd7c8335bdd3237ded442480a249dbcdb4ddfac5218799364a01a0f5c", + "sha256:15932ec5f224b0e35764dc156514533a4fca52dcfda0dfbe462a1a22b37efd59", + "sha256:18f87baa20e02e9277ad8960cd89b63c79c05caf106f4c959a9595c43f2a34a5", + "sha256:1a6420a36975e0073acaeee44ead260c1f6ea56812cfc6c31ec00c1c48197173", + "sha256:1b401e8b9aece651512e62c431181e6e83048a651698a727ea0eb0699e9f9b74", + "sha256:1d7b7b71bcb82d8713c7c2e9c5f061415598af5938666beded20d81fa23e7640", + "sha256:23750a9b8a329844ba1fe267ca456bb3184984da2880ed17ae641c5af8de3fef", + "sha256:23a059143c1393015c68936370cce11690f7294731904bdae47cc3e16d0b2474", + "sha256:26d9fd624649a10e4610fab2bc820e215a184d193e47d0be7fe53c1c8f67f370", + "sha256:291c9ce3929a75b45ce8ddde2aa7694fc8449f2bc8f5bd93adf021efaae2d10b", + "sha256:298e8b5d8087e0330aac211c85428c8761230ef46a1f2c516d6a2f67fb8803c5", + "sha256:2c7c4266c1b61eb429e8aeb7d8ed6a3bfe6c890a1788b18dbec090c35c6b93fa", + "sha256:2d68a8e8a3a816629283faf82358d8c93fe5bd974dd2704152394a3de4cec22a", + "sha256:344b89384c250ba6a4ce1786e04d01500e4dac0f4137ceebcaad12973c0ac0b3", + "sha256:3455ecc46ea443b5f7d9c2f946ce4017745e017b0d0f8b99c92564eff97e97f5", + "sha256:3d544a614055b131111bed6edfa1cb0fb082a7265761bcb03321f2dd7b5c6c48", + "sha256:3e5c26905aa651cc8c0ddc45e0e5dea2a1296f70bdc96af17aee9d0493280a17", + "sha256:3f5cc8c7bc99d2bbcd704cef165ca7d155cd6464c86cbda8339026a42d219397", + "sha256:4992266817169997854f81df7f6db7bdcda1609972d8ffd6919252f09ec3c0f6", + "sha256:4d55528ef13af4b4e074d067977b1f61408602f53ae4537dccf42ba665c2c7bd", + "sha256:576da63eae7809f375932bfcbca2cf20620a1915bf2fedce4b9cc8491eceefe3", + "sha256:58fc4d66ee349a23dbf08c7e964120dc9027059566e29cf0ce6205d590ed7eca", + "sha256:5b9bf77008f2c55dabbd099fd3ac87009471d223a1c7ebea36873d39511b780a", + "sha256:5e7996aed3f65667c6dcc8302a69368435a87c2364079a066750a2eac75ea01e", + "sha256:5f7487be65b9c2c510819e744e375bd41b929a97e5915c4852a82fbb085df62c", + "sha256:6388e4e95a26717b94a05ced084e19da4d92aca883f392dffcf8e48c8e221a24", + "sha256:65af12f70355de29e1092f319f85a3467f4005e959ab65129cb697169ce94b86", + "sha256:668d2b45d62c68c7a370ac3dce108ffda482b0a0f50abd8b4c604a813a59e08f", + "sha256:71333c22f7cf5f0480b59a0aef21f652cf9bbaa9679ad261b405b65a57511d1e", + "sha256:7150b83b3e3ddaac81a8bb6a9b5f93117674a0e7a2b5a5b32ab31fdfea6df27f", + "sha256:748e472345c3a82cfb462d0dff998a7bf43e621eed73374cb19f307e97e08a83", + "sha256:75dbfd41a61bc1fb0536bf7b1abf272dc115c53d4d77db770cd65d46d4520882", + "sha256:7618a082c55cf038eede4a918c1001cc8a4411dfe508dc762659bcd48d8f4c6e", + "sha256:780fcb855be29153901c67fc9c5633d48aebef21b90aa72812fa181d731c6b00", + "sha256:78d10c431073dc6ebceed35ab22948a016cc2b5120963c13a41e38bdde4a7212", + "sha256:7a3a3d3e4f1e3cd2a67b93a0b6ed0f2499e33f47cc568e3a0023e405abdc0ff1", + "sha256:7b6975d3763d0952c111700c0634968419268e6bbc0b55fe71138987fa66f309", + "sha256:80772e3bda6787510d9620bc0c7572be404a922f8ccdfd436bf6c3778119464c", + "sha256:80992eb20755701753e30a6952a96aa58f353d12a65ad3c9d48a8da5ec4690cf", + "sha256:841128a22e6ac04070a0f84776d07e9c38c4dcce8e28792a95e45fc621605517", + "sha256:861d25ae0985a1dd5297fee35f476b60c6029e2e6e19847d5b4d0a43a390b696", + "sha256:872f3dcaa8bf2245944861d7311179d2c0c9b2aaa7d3b464d99a7c2e401f01fa", + "sha256:87c93b25d538c433fb053da6228c6290117ba53ff6a537c133b0f2087948a582", + "sha256:8856aa76839dc234d3469f1e270918ce6bec1d6a601eba928f45d68a15f04fc3", + "sha256:885e023e73ce09b11b89ab91fc60f35d80878d2c19d6213a32b42ff36543c291", + "sha256:899b5e7e2d5a8bc92aa533c2d4e55e5ebba095c485568a5e4bedbc163421259a", + "sha256:8ce8caa29ebbdcde67e5fd652c811d34bc01f249dbc0d61e5cc4db05ae79a83b", + "sha256:8e1c68303ccf7fceb50fbab79064a2636119fd9aca121f28453709283dbca727", + "sha256:8e7e2b3577e97fa43c2c2b12a16139b2cedbd0770235d5179c0412b4794efd9b", + "sha256:92f05fc7d832e970047662b3440b190d24ea04f8d3c760e33e7163b67308c878", + "sha256:97f5811df21703446b42303475b8b855ee07d6ab6cdf8565eff115540624f25d", + "sha256:9affee8cb1ec453382c27eb9043378ab32f49cd4bc24a24275f5c39bf186c279", + "sha256:a2da4a8c6d465fde36cea7d54bf47b5cf089073452f0e47c8632ecb9dec23c07", + "sha256:a6903cdca64f1e301af9be424798328c1fe3b4b14aede35f04510989fc72f012", + "sha256:a8ab1adf04ae2d6d65835995218fd3f3eb644fe20655ca8ee233e2c7270ff53b", + "sha256:a8edd467551c1102dc0f5754ab55cd0703431cd3044edf8c8e7d9208d63fa453", + "sha256:ac00c41dd315d147b129976204839ca9de699d83519ff1272afbe4fb9d362d12", + "sha256:ad277f74b1c164f7248afa968700e410651eb858d7c160d109fb451dc45a2f09", + "sha256:ae46a50d235f1631d9ec4670503f7b30405103034830bc13df29fd947207f795", + "sha256:afe6b5a04b2ab1aa89bad32ca47bf71358e7302a06fdfdad857389dca8fb5f04", + "sha256:b1cb078f54af0abd835ca76f93a3152565b73be0f056264da45117d0adf5e99c", + "sha256:b25136212a3d064a8f0b9ebbb6c57094c5229e0de76d15c79b76feff26aeb7b8", + "sha256:b3226b246facae14909b465061ddcfa2dfeadb6a64f407f24300d42d69bcb1a1", + "sha256:b98e75b21fc2ba5285aef8efaf34131d16af1c38df36bdca2f50634bea2d3060", + "sha256:bbd7b24d108509a1b9b6679fcc1166a7dd031dbef1f3c2c73788f42e3ebb3beb", + "sha256:bed57543c99249ab3a4586ddc8786529fbc33309e5e8a1351802a06ca2baf4c2", + "sha256:c0583f69522732bdd79dca4cd3873e63a29acf4a299769c7541f2ca1e4dd4bc6", + "sha256:c1e0e9916301e3b3d970814b1439ca59487f0616d30f36a44cead66ee1748c31", + "sha256:c651847545422c8131660704c58606d841e228ed576c8f1666d98b3d318f89da", + "sha256:c7853f27195598e550fe089f78f0732c66ee1d1f0eaae8ad081589a5a2f5d4af", + "sha256:cbae50d352e4717ffc22c566afc2d0da744380e87ed44a144508e3fb9114a3f4", + "sha256:cdbed8f21204398f47de39b0a9b180d7e571f02dfb18bf5f1b618e238454b685", + "sha256:d08395595c42bcd82c3608762ce734504c6d025eef1c06f42326a6023a584186", + "sha256:d4639111e73997567343df6551da9dd90d66aece1b9fc26c786d328439488103", + "sha256:d63787f289944cc4bde518ad2b5e70a4f0d6e2ce76324635359c74c113fd188f", + "sha256:d6d5f061f6a2aa55790b9e64a23dfd87b6664ab56e24cd06c78eb43986cb260b", + "sha256:d7865df1fb564092bcf46dac61b5def25342faf6352e4bc0e61a286e3fa26a3d", + "sha256:db6585b600b2e76e98131e0ac0e5195759082b51687ad0c94505970c90718f4a", + "sha256:e36d7369363d2707d5f68950a64c4e025991eb0177db01ccb6aa6facae48b69f", + "sha256:e7947d9a6264c727a556541b1630296bbd5d0a05068d21c38dde8e7a1c703ef0", + "sha256:eb2d59bc196e6d3b1827c7db06c1a898bfa0787c0574af398e65ccf2e97c0fbe", + "sha256:ee9c2f6ca9774c2c24bbf7b23086264e6b5fa178201450535ec0859739e6f78d", + "sha256:f4760e1b02173f4155203054f77a5dc0b4078de7645c922b208d28e7eb99f3e2", + "sha256:f70bec8a14a692be6dbe7ce8aab303e88df891cbd4a39af091f90b6702e28055", + "sha256:f869e34d2326e417baee430ae998e91412cc8e7fdd83d979277a90a0e79a5b47", + "sha256:f8b9a7cd381970e64849070aca7c32d53ab7d96c66db6c2ef7aa23c6e803f514", + "sha256:f99d74ddf9d3b6126b509e81865f89bd1283e3fc1b568b68cd7bd9dfa15583d7", + "sha256:f9e7e493ded7042712a374471203dd43ae3fff5b81e3de1a0513fa241af9fd41", + "sha256:fc72ae476732cdb7b2c1acb5af23b478b8a0d4b6fcf19b90dd150291e0d5b26b", + "sha256:fccbf0cd3411719e4c9426755df90bf3449d9fc5a89f077f4a7f1abd4f70c910", + "sha256:ffcf18ad3edf1c170e27e88b10282a2c449aa0358659592462448d71b2000cfc" ], "markers": "python_version >= '3.8'", - "version": "==0.10.3" + "version": "==0.10.0" }, "ruamel.yaml": { "hashes": [ From 803d7a629e150edb9d84607281276bf3b71a4efa Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 30 Aug 2023 13:50:05 +0200 Subject: [PATCH 11/30] Only allow to edit open verification requests --- apps/accounts/views.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index dc1fbc76..706ca325 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -289,6 +289,14 @@ def dispatch(self, request, *args, **kwargs): except ProviderRequest.DoesNotExist: raise Http404("Page not found") + if pr.status != ProviderRequestStatus.OPEN: + messages.error( + self.request, "This verification request cannot be edited at this time" + ) + return HttpResponseRedirect( + reverse("provider_request_detail", args=[pr.pk]) + ) + # only admins and creators can access for editing existing requests if not request.user.is_admin and request.user.id != pr.created_by.id: raise Http404("Page not found") From 5a7d49a9f703179d2f79e06f6c9b740f3eeaf319 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 30 Aug 2023 14:38:10 +0200 Subject: [PATCH 12/30] Fix setting services from slugs --- apps/accounts/models/provider_request.py | 8 ++++ apps/accounts/tests/test_provider_request.py | 40 ++++++++++++++++++++ apps/accounts/views.py | 4 +- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/apps/accounts/models/provider_request.py b/apps/accounts/models/provider_request.py index 9642411f..cfddd309 100644 --- a/apps/accounts/models/provider_request.py +++ b/apps/accounts/models/provider_request.py @@ -127,6 +127,14 @@ def from_kwargs(**kwargs) -> "ProviderRequest": pr_data.setdefault("status", ProviderRequestStatus.OPEN.value) return ProviderRequest.objects.create(**pr_data) + def set_services_from_slugs(self, service_slugs: Iterable[str]) -> None: + """ + Given list of service slugs (corresponding to Tag slugs) + apply matching services to the ProviderRequest object + """ + services = Service.objects.filter(slug__in=service_slugs) + self.services.set(services) + @classmethod def get_service_choices(cls) -> List[Tuple[int, str]]: """ diff --git a/apps/accounts/tests/test_provider_request.py b/apps/accounts/tests/test_provider_request.py index bf6bd860..142bbe5e 100644 --- a/apps/accounts/tests/test_provider_request.py +++ b/apps/accounts/tests/test_provider_request.py @@ -742,3 +742,43 @@ def test_wizard_records_if_location_import_needed( pr_from_db = models.ProviderRequest.objects.filter(id=pr.id).first() assert pr_from_db.location_import_required is True + + +@pytest.mark.django_db +@override_flag("provider_request", active=True) +def test_new_submission_doesnt_modify_available_services( + user, + client, + wizard_form_org_details_data, + wizard_form_org_location_data, + wizard_form_services_data, + wizard_form_evidence_data, + wizard_form_network_data, + wizard_form_consent, + wizard_form_preview, +): + # given: existing list of available services + services = models.Service.objects.all() + + # given: valid form data and authenticated user + form_data = [ + wizard_form_org_details_data, + wizard_form_org_location_data, + wizard_form_services_data, + wizard_form_evidence_data, + wizard_form_network_data, + wizard_form_consent, + wizard_form_preview, + ] + client.force_login(user) + + # when: a multi step submission has been successfully completed + response = _create_provider_request(client, form_data) + + pr = response.context_data["providerrequest"] + pr_from_db = models.ProviderRequest.objects.filter(id=pr.id).first() + + # then: all services in the new request already existed in the db + assert all(service in services for service in pr_from_db.services.all()) + # then: no new services were created in the db + assert set(models.Service.objects.all()) == set(services) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 706ca325..2b08aba6 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -341,8 +341,8 @@ def done(self, form_list, form_dict, **kwargs): # process SERVICES form: assign services to ProviderRequest services_form = form_dict[steps.SERVICES.value] - services = services_form.cleaned_data["services"] - pr.services.set(services) + service_slugs = services_form.cleaned_data["services"] + pr.set_services_from_slugs(service_slugs) pr.created_by = self.request.user pr.save() From 57c01ae02eb249a0aead5767f79d65d6288d21a7 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 30 Aug 2023 14:55:25 +0200 Subject: [PATCH 13/30] Add admin action: request additional changes in a submitted verification request --- apps/accounts/admin.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py index bef13745..021bd520 100644 --- a/apps/accounts/admin.py +++ b/apps/accounts/admin.py @@ -1381,7 +1381,7 @@ class ProviderRequest(ActionInChangeFormMixin, admin.ModelAdmin): "network_import_required", "missing_network_explanation", ) - actions = ["mark_approved", "mark_rejected", "mark_removed"] + actions = ["mark_approved", "mark_open", "mark_rejected", "mark_removed"] change_form_template = "admin/provider_request/change_form.html" def send_approval_email(self, provider_request, request): @@ -1484,3 +1484,21 @@ def mark_removed(self, request, queryset): """ ) self.message_user(request, message=message, level=messages.SUCCESS) + + @admin.action(description="Request changes", permissions=["change"]) + def mark_open(self, request, queryset): + for provider_request in queryset: + provider_request.status = ProviderRequestStatus.OPEN.value + provider_request.save() + + message = mark_safe( + f""" + Request id {provider_request.id} for provider: \ + {provider_request.name} has changed the status to OPEN, + making it available for editing and re-submitting. + + They creator of this request has not been contacted yet - + you will need to contact them if appropriate. + """ + ) + self.message_user(request, message=message, level=messages.SUCCESS) From cb73efc2571db247324f8db7b3501edc7662e448 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 30 Aug 2023 14:55:49 +0200 Subject: [PATCH 14/30] Explicitly set status to pending review on submitted/resubmitted verification request --- apps/accounts/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 2b08aba6..f48ffc02 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -386,6 +386,10 @@ def done(self, form_list, form_dict, **kwargs): consent.request = pr consent.save() + # set status + pr.status = ProviderRequestStatus.PENDING_REVIEW.value + pr.save() + # send an email notification to the author and green web staff self._send_notification_email(pr) From 66faf0a957c4c814d429ee9188672658682fc35e Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 6 Sep 2023 11:56:49 +0100 Subject: [PATCH 15/30] Add TODO + link to relevant docs to fix the preview step later --- apps/accounts/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index f48ffc02..358fa03b 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -434,6 +434,10 @@ def _get_data_for_preview(self): will include extra (empty) forms as defined per formset factory. Templates should use `formset.initial_forms` for rendering non-empty forms. """ + # TODO: fix passing data for the preview step + # PROBLEM: ModelFormSets take initial value truncated to extra forms only! + # that's why we only see 1 instance passed to the formset + # https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#id2 preview_forms = {} # iterate over all forms without the last one (PREVIEW) for step, form in self.FORMS[:-1]: From 962e86bb000e40dd54fe1d6a55633bf265120253 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 6 Sep 2023 15:47:38 +0100 Subject: [PATCH 16/30] Reorganize tests & add a basic test case for accessing edit view --- apps/accounts/tests/test_admin.py | 2 +- apps/accounts/tests/test_provider_portal.py | 21 ++++++++++++ apps/accounts/tests/test_provider_request.py | 36 ++++++++------------ 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/apps/accounts/tests/test_admin.py b/apps/accounts/tests/test_admin.py index ed941a1a..4d14b9b7 100644 --- a/apps/accounts/tests/test_admin.py +++ b/apps/accounts/tests/test_admin.py @@ -559,7 +559,7 @@ def test_provider_request_accessible_by_admin( request.user = greenweb_staff_user inline_instances = pr_admin.get_inline_instances(request, pr) - assert len(inline_instances) == 5 + assert len(inline_instances) == 4 assert pr_admin.has_add_permission(request) assert pr_admin.has_view_permission(request) diff --git a/apps/accounts/tests/test_provider_portal.py b/apps/accounts/tests/test_provider_portal.py index 5ea5e1f7..67c108f2 100644 --- a/apps/accounts/tests/test_provider_portal.py +++ b/apps/accounts/tests/test_provider_portal.py @@ -8,6 +8,27 @@ import pytest +@pytest.mark.django_db +@override_flag("provider_request", active=True) +def test_provider_portal_home_view_displays_only_authored_requests(client): + # given: 3 provider requests exist, created by different users + pr1 = ProviderRequestFactory.create() + pr2 = ProviderRequestFactory.create() + pr3 = ProviderRequestFactory.create() + + # when: accessing the list view as the author of pr2 + client.force_login(pr2.created_by) + response = client.get(reverse("provider_portal_home")) + + # then: link to detail view of pr2 is displayed + assert response.status_code == 200 + assert f'href="{pr2.get_absolute_url()}"'.encode() in response.content + + # then: links to pr1 and pr3 are not displayed + assert f'href="{pr1.get_absolute_url()}"'.encode() not in response.content + assert f'href="{pr3.get_absolute_url()}"'.encode() not in response.content + + @pytest.mark.django_db @override_flag("provider_request", active=True) def test_provider_portal_home_view_returns_only_unapproved_requests(client): diff --git a/apps/accounts/tests/test_provider_request.py b/apps/accounts/tests/test_provider_request.py index 142bbe5e..b015b8d3 100644 --- a/apps/accounts/tests/test_provider_request.py +++ b/apps/accounts/tests/test_provider_request.py @@ -276,27 +276,6 @@ def test_detail_view_forbidden_for_others(client, user): assert response.status_code == 404 -@pytest.mark.django_db -@override_flag("provider_request", active=True) -def test_provider_portal_home_view_displays_only_authored_requests(client): - # given: 3 provider requests exist, created by different users - pr1 = ProviderRequestFactory.create() - pr2 = ProviderRequestFactory.create() - pr3 = ProviderRequestFactory.create() - - # when: accessing the list view as the author of pr2 - client.force_login(pr2.created_by) - response = client.get(urls.reverse("provider_portal_home")) - - # then: link to detail view of pr2 is displayed - assert response.status_code == 200 - assert f'href="{pr2.get_absolute_url()}"'.encode() in response.content - - # then: links to pr1 and pr3 are not displayed - assert f'href="{pr1.get_absolute_url()}"'.encode() not in response.content - assert f'href="{pr3.get_absolute_url()}"'.encode() not in response.content - - @pytest.mark.django_db @override_flag("provider_request", active=True) def test_wizard_view_happy_path( @@ -782,3 +761,18 @@ def test_new_submission_doesnt_modify_available_services( assert all(service in services for service in pr_from_db.services.all()) # then: no new services were created in the db assert set(models.Service.objects.all()) == set(services) + + +@pytest.mark.django_db +@override_flag("provider_request", active=True) +def test_edit_view_accessible_by_creator(client): + # given: an approved provider request + pr = ProviderRequestFactory.create() + loc = ProviderRequestLocationFactory.create(request=pr) + + # when: accessing its edit view by the creator + client.force_login(pr.created_by) + response = client.get(urls.reverse("provider_request_edit", args=[str(pr.id)])) + + # then: page for the correct provider request is rendered + assert response.status_code == 200 From 2089c01b5cb0852bed6184b449659071a2e3e986 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 6 Sep 2023 15:48:19 +0100 Subject: [PATCH 17/30] Replace ProviderRequestConsent with extending ProviderRequest object with additional fields --- apps/accounts/admin.py | 14 +------ apps/accounts/forms.py | 6 +-- .../0060_provider_request_consent.py | 42 +++++++++++++++++++ apps/accounts/models/provider_request.py | 22 +++------- .../provider_portal/request_detail.html | 10 ++--- apps/accounts/views.py | 11 ++--- conftest.py | 9 ---- 7 files changed, 63 insertions(+), 51 deletions(-) create mode 100644 apps/accounts/migrations/0060_provider_request_consent.py diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py index 021bd520..a24b9edd 100644 --- a/apps/accounts/admin.py +++ b/apps/accounts/admin.py @@ -75,7 +75,6 @@ ProviderRequestIPRange, ProviderRequestLocation, ProviderRequestEvidence, - ProviderRequestConsent, ProviderRequestStatus, Service, ) @@ -1314,16 +1313,6 @@ class ProviderRequestLocationInline(AdminOnlyTabularInline): extra = 0 -class ProviderRequestConsentInline(AdminOnlyTabularInline): - model = ProviderRequestConsent - extra = 0 - max_num = 1 - readonly_fields = ( - "data_processing_opt_in", - "newsletter_opt_in", - ) - - class ActionInChangeFormMixin(object): """ Adds custom admin actions @@ -1367,7 +1356,6 @@ class ProviderRequest(ActionInChangeFormMixin, admin.ModelAdmin): ProviderRequestEvidenceInline, ProviderRequestIPRangeInline, ProviderRequestASNInline, - ProviderRequestConsentInline, ] formfield_overrides = {TaggableManager: {"widget": LabelWidget(model=Service)}} empty_value_display = "(empty)" @@ -1380,6 +1368,8 @@ class ProviderRequest(ActionInChangeFormMixin, admin.ModelAdmin): "location_import_required", "network_import_required", "missing_network_explanation", + "newsletter_opt_in", + "data_processing_opt_in", ) actions = ["mark_approved", "mark_open", "mark_rejected", "mark_removed"] change_form_template = "admin/provider_request/change_form.html" diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 2f587cc7..2c7991a5 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -515,7 +515,7 @@ class ConsentForm(forms.ModelForm): """ Part of multi-step registration form (screen 5). - Gathers consent information. + Set of agreements that the user consents to (or not) to by submitting the request. """ data_processing_opt_in = forms.BooleanField( @@ -543,8 +543,8 @@ class ConsentForm(forms.ModelForm): ) class Meta: - model = ac_models.ProviderRequestConsent - exclude = ["request"] + model = ac_models.ProviderRequest + fields = ["data_processing_opt_in", "newsletter_opt_in"] class LocationForm(forms.ModelForm): diff --git a/apps/accounts/migrations/0060_provider_request_consent.py b/apps/accounts/migrations/0060_provider_request_consent.py new file mode 100644 index 00000000..cb81d102 --- /dev/null +++ b/apps/accounts/migrations/0060_provider_request_consent.py @@ -0,0 +1,42 @@ +# Generated by Django 3.2.20 on 2023-09-06 12:09 + +from django.db import migrations, models + + +def populate_pr_consent(apps, schema_editor): + """ + Integrate ProviderRequestConsent objects into ProviderRequest + after it was extended with new fields + """ + db_alias = schema_editor.connection.alias + ProviderRequestConsent = apps.get_model("accounts", "ProviderRequestConsent") + for consent in ProviderRequestConsent.objects.using(db_alias).all(): + pr = consent.request + pr.data_processing_opt_in = consent.data_processing_opt_in + pr.newsletter_opt_in = consent.newsletter_opt_in + pr.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("accounts", "0059_merge_20230712_1415"), + ] + + operations = [ + migrations.AddField( + model_name="providerrequest", + name="data_processing_opt_in", + field=models.BooleanField( + default=False, verbose_name="Data processing consent" + ), + ), + migrations.AddField( + model_name="providerrequest", + name="newsletter_opt_in", + field=models.BooleanField(default=False, verbose_name="Newsletter signup"), + ), + migrations.RunPython(code=populate_pr_consent, reverse_code=lambda *args: ...), + migrations.DeleteModel( + name="ProviderRequestConsent", + ), + ] diff --git a/apps/accounts/models/provider_request.py b/apps/accounts/models/provider_request.py index cfddd309..dab94f6d 100644 --- a/apps/accounts/models/provider_request.py +++ b/apps/accounts/models/provider_request.py @@ -76,7 +76,6 @@ class ProviderRequest(TimeStampedModel): status = models.CharField( choices=ProviderRequestStatus.choices, max_length=255, - default=ProviderRequestStatus.PENDING_REVIEW.value, ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True @@ -102,6 +101,12 @@ class ProviderRequest(TimeStampedModel): ) location_import_required = models.BooleanField(default=False) network_import_required = models.BooleanField(default=False) + data_processing_opt_in = models.BooleanField( + default=False, verbose_name="Data processing consent" + ) + newsletter_opt_in = models.BooleanField( + default=False, verbose_name="Newsletter signup" + ) def __str__(self) -> str: return f"{self.name}" @@ -334,18 +339,3 @@ def clean(self) -> None: raise ValidationError(f"{reason}, you haven't submitted either.") if self.link and bool(self.file): raise ValidationError(f"{reason}, you've attempted to submit both.") - - -class ProviderRequestConsent(models.Model): - """ - Set of agreements that the user consents to (or not) to by submitting the request. - """ - - data_processing_opt_in = models.BooleanField(default=False) - newsletter_opt_in = models.BooleanField(default=False) - request = models.ForeignKey(ProviderRequest, on_delete=models.CASCADE) - - def __str__(self) -> str: - data_processing = f"Data processing: {self.data_processing_opt_in}" - newsletter = f"Newsletter signup: {self.newsletter_opt_in}" - return f"{data_processing}, {newsletter}" diff --git a/apps/accounts/templates/provider_portal/request_detail.html b/apps/accounts/templates/provider_portal/request_detail.html index 02e4c52d..72e33350 100644 --- a/apps/accounts/templates/provider_portal/request_detail.html +++ b/apps/accounts/templates/provider_portal/request_detail.html @@ -128,12 +128,10 @@

Consent

The following consent was submitted: - {% for consent in object.providerrequestconsent_set.all %} -
- Data processing opt-in: {{ consent.data_processing_opt_in|yesno:"Yes,No,-"}}
- Newsletter opt-in: {{ consent.newsletter_opt_in|yesno:"Yes,No,-"}} -
- {% endfor %} +
+ Data processing opt-in: {{ object.data_processing_opt_in|yesno:"Yes,No,-"}}
+ Newsletter opt-in: {{ object.newsletter_opt_in|yesno:"Yes,No,-"}} +
diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 358fa03b..c3061525 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -382,9 +382,11 @@ def done(self, form_list, form_dict, **kwargs): # process CONSENT form consent_form = form_dict[steps.CONSENT.value] - consent = consent_form.save(commit=False) - consent.request = pr - consent.save() + data_processing_opt_in = consent_form.cleaned_data.get("data_processing_opt_in") + newsletter_opt_in = consent_form.cleaned_data.get("newsletter_opt_in") + pr.data_processing_opt_in = bool(data_processing_opt_in) + pr.newsletter_opt_in = bool(newsletter_opt_in) + pr.save() # set status pr.status = ProviderRequestStatus.PENDING_REVIEW.value @@ -481,7 +483,6 @@ def get_instance_dict(self, request_id): evidence_qs = pr_instance.providerrequestevidence_set.all() asn_qs = pr_instance.providerrequestasn_set.all() ip_qs = pr_instance.providerrequestiprange_set.all() - consent = pr_instance.providerrequestconsent_set.get() instance_dict = { self.Steps.ORG_DETAILS.value: pr_instance, @@ -496,7 +497,7 @@ def get_instance_dict(self, request_id): "asns": asn_qs, "extra": pr_instance, }, - self.Steps.CONSENT.value: consent, + self.Steps.CONSENT.value: pr_instance, } return instance_dict diff --git a/conftest.py b/conftest.py index 25b757b2..425fa710 100644 --- a/conftest.py +++ b/conftest.py @@ -109,15 +109,6 @@ class Meta: model = ac_models.ProviderRequestASN -class ProviderRequestConsentFactory(factory.django.DjangoModelFactory): - data_processing_opt_in = True - newsletter_opt_in = factory.Faker("random_element", elements=[True, False]) - request = factory.SubFactory(ProviderRequestFactory) - - class Meta: - model = ac_models.ProviderRequestConsent - - @pytest.fixture def provider_groups(): return auth_models.Group.objects.filter(name__in=["datacenter", "hostingprovider"]) From 555a9bac24ba4e742e7341fcf315c71dfbfad6cb Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 6 Sep 2023 16:40:08 +0100 Subject: [PATCH 18/30] Add tests for accessing the edit view of a verification request --- apps/accounts/tests/test_provider_request.py | 58 +++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/apps/accounts/tests/test_provider_request.py b/apps/accounts/tests/test_provider_request.py index b015b8d3..9b4d7575 100644 --- a/apps/accounts/tests/test_provider_request.py +++ b/apps/accounts/tests/test_provider_request.py @@ -766,9 +766,9 @@ def test_new_submission_doesnt_modify_available_services( @pytest.mark.django_db @override_flag("provider_request", active=True) def test_edit_view_accessible_by_creator(client): - # given: an approved provider request + # given: an open provider request pr = ProviderRequestFactory.create() - loc = ProviderRequestLocationFactory.create(request=pr) + ProviderRequestLocationFactory.create(request=pr) # when: accessing its edit view by the creator client.force_login(pr.created_by) @@ -776,3 +776,57 @@ def test_edit_view_accessible_by_creator(client): # then: page for the correct provider request is rendered assert response.status_code == 200 + + +@pytest.mark.django_db +@override_flag("provider_request", active=True) +def test_edit_view_accessible_by_admin(client, greenweb_staff_user): + # given: an open provider request + pr = ProviderRequestFactory.create() + ProviderRequestLocationFactory.create(request=pr) + + # when: accessing its edit view by Green Web staff + client.force_login(greenweb_staff_user) + response = client.get(urls.reverse("provider_request_edit", args=[str(pr.id)])) + + # then: page for the correct provider request is rendered + assert response.status_code == 200 + + +@pytest.mark.django_db +@override_flag("provider_request", active=True) +def test_edit_view_inaccessible_by_other_users(client, user): + # given: an open provider request + pr = ProviderRequestFactory.create() + ProviderRequestLocationFactory.create(request=pr) + + # when: accessing its edit view by regular users other than the creator + client.force_login(user) + response = client.get(urls.reverse("provider_request_edit", args=[str(pr.id)])) + + # then: we pretend this request does not exist and show 404 + assert response.status_code == 404 + + +@pytest.mark.django_db +@override_flag("provider_request", active=True) +@pytest.mark.parametrize( + "request_status,status_code", + [ + (models.ProviderRequestStatus.OPEN, 200), + (models.ProviderRequestStatus.PENDING_REVIEW, 302), + (models.ProviderRequestStatus.APPROVED, 302), + ], + ids=["open-accessible", "pending_review-inaccessible", "approved-inaccessible"], +) +def test_edit_view_accessible_for_given_status(client, request_status, status_code): + # given: a provider request with a given status + pr = ProviderRequestFactory.create(status=request_status) + ProviderRequestLocationFactory.create(request=pr) + + # when: accessing the edit view by its creator + client.force_login(pr.created_by) + response = client.get(urls.reverse("provider_request_edit", args=[str(pr.id)])) + + # then: response with an expected status code is returned + assert response.status_code == status_code From 1c63a67484a8c93ca18101368841c39f91350734 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 6 Sep 2023 17:55:54 +0100 Subject: [PATCH 19/30] WIP: add end-to-end test for updating PR using wizard form --- apps/accounts/tests/test_provider_request.py | 103 ++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/apps/accounts/tests/test_provider_request.py b/apps/accounts/tests/test_provider_request.py index 9b4d7575..1b800e39 100644 --- a/apps/accounts/tests/test_provider_request.py +++ b/apps/accounts/tests/test_provider_request.py @@ -302,7 +302,6 @@ def test_wizard_view_happy_path( client.force_login(user) # when: submitting form data for consecutive wizard steps - for step, data in enumerate(form_data, 1): response = client.post(urls.reverse("provider_registration"), data, follow=True) # then: for all steps except the last one, @@ -830,3 +829,105 @@ def test_edit_view_accessible_for_given_status(client, request_status, status_co # then: response with an expected status code is returned assert response.status_code == status_code + + +@pytest.mark.django_db +@override_flag("provider_request", active=True) +def test_edit_view_displays_form_with_prepopulated_data(client): + # given: an open provider request + pr = ProviderRequestFactory.create() + ProviderRequestLocationFactory.create(request=pr) + + # when: accessing the edit view by its creator + client.force_login(pr.created_by) + response = client.get(urls.reverse("provider_request_edit", args=[str(pr.id)])) + + # then: rendered form (we only check the first page) + # contains pre-filled data about the original request + form = response.context_data["form"] + assert form.instance == pr + + expected_initial = { + "name": pr.name, + "website": pr.website, + "description": pr.description, + "authorised_by_org": pr.authorised_by_org, + } + assert form.initial == expected_initial + + +@pytest.mark.django_db +@override_flag("provider_request", active=True) +def test_editing_pr_updates_original_submission( + client, + wizard_form_org_details_data, + wizard_form_org_location_data, + wizard_form_services_data, + wizard_form_evidence_data, + wizard_form_network_data, + wizard_form_consent, + wizard_form_preview, +): + # given: an open provider request + pr = ProviderRequestFactory.create() + + loc1 = ProviderRequestLocationFactory.create(request=pr) + loc2 = ProviderRequestLocationFactory.create(request=pr) + + ev1 = ProviderRequestEvidenceFactory.create(request=pr) + ev2 = ProviderRequestEvidenceFactory.create(request=pr) + + ip1 = ProviderRequestIPRangeFactory.create(request=pr) + ip2 = ProviderRequestIPRangeFactory.create(request=pr) + ip3 = ProviderRequestIPRangeFactory.create(request=pr) + + asn = ProviderRequestASNFactory.create(request=pr) + + # given: valid form data for consecutive wizard steps + # (to override the initial data) + form_data = [ + wizard_form_org_details_data, + wizard_form_org_location_data, + wizard_form_services_data, + wizard_form_evidence_data, + wizard_form_network_data, + wizard_form_consent, + wizard_form_preview, + ] + + # given: URL of the edit view of the existing PR + edit_url = urls.reverse("provider_request_edit", args=[str(pr.id)]) + + # when: accessing the edit view by its creator + client.force_login(pr.created_by) + response = client.get(edit_url) + + # then: ORG_DETAILS form is bound with an instance, initial data is displayed + assert response.context_data["wizard"]["steps"].current == "0" + assert response.context_data["form"].instance == pr + assert response.context_data["form"].initial == { + "name": pr.name, + "website": pr.website, + "description": pr.description, + "authorised_by_org": pr.authorised_by_org, + } + # when: submitting ORG_DETAILS form with overridden data + response = client.post(edit_url, wizard_form_org_details_data, follow=True) + + # then: wizard proceeds, LOCATIONS formset is displayed with bound queryset and initial data + locations_formset = response.context_data["form"].forms["locations"] + assert all([loc in locations_formset.queryset for loc in [loc1, loc2]]) + assert locations_formset.forms[0].initial == {"name": loc1.name, "city": loc1.city, "country": loc1.country} + assert locations_formset.forms[1].initial == {"name": loc2.name, "city": loc2.city, "country": loc2.country} + # when: submitting LOCATIONS form with overridden data + response = client.post(edit_url, wizard_form_org_location_data, follow=True) + + # then: wizard proceeds, SERVICES form is displayed with bound instance and initial data + + + # then: submitting the final step redirects to the detail view + assert response.resolver_match.func.view_class is views.ProviderRequestDetailView + # then: a ProviderRequest object is updated in the db + pr_id = response.context_data["providerrequest"].id + updated_pr = models.ProviderRequest.objects.get(id=pr_id) + assert updated_pr # TODO: verify fields have changed From bfba7050c4dd21a657c4c97c87019a3d5b2f1515 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 6 Sep 2023 19:32:21 +0100 Subject: [PATCH 20/30] Add an uber end-to-end test for updating PR using the wizard form --- apps/accounts/tests/test_provider_request.py | 143 +++++++++++++++---- 1 file changed, 116 insertions(+), 27 deletions(-) diff --git a/apps/accounts/tests/test_provider_request.py b/apps/accounts/tests/test_provider_request.py index 1b800e39..bde16a49 100644 --- a/apps/accounts/tests/test_provider_request.py +++ b/apps/accounts/tests/test_provider_request.py @@ -52,10 +52,14 @@ def wizard_form_org_location_data(): """ return { "provider_request_wizard_view-current_step": "1", - "locations__1-TOTAL_FORMS": "1", + "locations__1-TOTAL_FORMS": "3", "locations__1-INITIAL_FORMS": "0", "locations__1-0-country": faker.country_code(), "locations__1-0-city": faker.city(), + "locations__1-1-country": faker.country_code(), + "locations__1-1-city": faker.city(), + "locations__1-2-country": faker.country_code(), + "locations__1-2-city": faker.city(), "extra__1-location_import_required": "True", } @@ -868,8 +872,18 @@ def test_editing_pr_updates_original_submission( wizard_form_consent, wizard_form_preview, ): + """ + This is an end-to-end test verifying that: + - edit view for an existing ProviderRequest displays + initial data correctly for each step + - ModelForms and ModelFormsets in consecutive steps are bound + to correct "instance" or "queryset" respectively. + + Initial data for ProviderRequest is created in the test, + data used for updating the object is injected as wizard_form_* fixtures. + """ # given: an open provider request - pr = ProviderRequestFactory.create() + pr = ProviderRequestFactory.create(services=["service1", "service2"]) loc1 = ProviderRequestLocationFactory.create(request=pr) loc2 = ProviderRequestLocationFactory.create(request=pr) @@ -883,18 +897,6 @@ def test_editing_pr_updates_original_submission( asn = ProviderRequestASNFactory.create(request=pr) - # given: valid form data for consecutive wizard steps - # (to override the initial data) - form_data = [ - wizard_form_org_details_data, - wizard_form_org_location_data, - wizard_form_services_data, - wizard_form_evidence_data, - wizard_form_network_data, - wizard_form_consent, - wizard_form_preview, - ] - # given: URL of the edit view of the existing PR edit_url = urls.reverse("provider_request_edit", args=[str(pr.id)]) @@ -903,31 +905,118 @@ def test_editing_pr_updates_original_submission( response = client.get(edit_url) # then: ORG_DETAILS form is bound with an instance, initial data is displayed - assert response.context_data["wizard"]["steps"].current == "0" - assert response.context_data["form"].instance == pr - assert response.context_data["form"].initial == { - "name": pr.name, - "website": pr.website, - "description": pr.description, - "authorised_by_org": pr.authorised_by_org, - } + org_details_form = response.context_data["form"] + assert org_details_form.instance == pr + assert org_details_form.initial == { + "name": pr.name, + "website": pr.website, + "description": pr.description, + "authorised_by_org": pr.authorised_by_org, + } # when: submitting ORG_DETAILS form with overridden data response = client.post(edit_url, wizard_form_org_details_data, follow=True) - + # then: wizard proceeds, LOCATIONS formset is displayed with bound queryset and initial data locations_formset = response.context_data["form"].forms["locations"] - assert all([loc in locations_formset.queryset for loc in [loc1, loc2]]) - assert locations_formset.forms[0].initial == {"name": loc1.name, "city": loc1.city, "country": loc1.country} - assert locations_formset.forms[1].initial == {"name": loc2.name, "city": loc2.city, "country": loc2.country} + assert set(locations_formset.queryset) == set([loc1, loc2]) + assert locations_formset.forms[0].initial == { + "name": loc1.name, + "city": loc1.city, + "country": loc1.country, + } + assert locations_formset.forms[1].initial == { + "name": loc2.name, + "city": loc2.city, + "country": loc2.country, + } # when: submitting LOCATIONS form with overridden data response = client.post(edit_url, wizard_form_org_location_data, follow=True) # then: wizard proceeds, SERVICES form is displayed with bound instance and initial data + services_form = response.context_data["form"] + assert services_form.instance == pr + assert services_form.initial == {"services": ["service1", "service2"]} + # when: submitting SERVICES form with overridden data + response = client.post(edit_url, wizard_form_services_data, follow=True) + + # then: wizards proceeds, EVIDENCE formset is displayed with bound queryset and initial data + evidence_formset = response.context_data["form"] + assert set(evidence_formset.queryset) == set([ev1, ev2]) + # we strip expected initial data from "file" key for comparison purposes + # because {'file': } != {'file': } + ev1_initial = { + "title": ev1.title, + "description": ev1.description, + "link": ev1.link, + "type": ev1.type, + "public": ev1.public, + } + ev2_initial = { + "title": ev2.title, + "description": ev2.description, + "link": ev2.link, + "type": ev2.type, + "public": ev2.public, + } + assert ev1_initial.items() <= evidence_formset.forms[0].initial.items() + assert ev2_initial.items() <= evidence_formset.forms[1].initial.items() + + # when: submitting EVIDENCE step with overridden data + response = client.post(edit_url, wizard_form_evidence_data, follow=True) + + # then: wizard proceeds, NETWORK form is displayed + # and child forms/formsets have queryset/instance and initial data assigned + network_form = response.context_data["form"] + ip_formset = network_form.forms["ips"] + assert set(ip_formset.queryset) == set([ip1, ip2, ip3]) + + asn_formset = network_form.forms["asns"] + assert set([asn]) == set(asn_formset.queryset) + + extra_network_form = network_form.forms["extra"] + assert extra_network_form.instance == pr + assert extra_network_form.initial == { + "missing_network_explanation": pr.missing_network_explanation, + "network_import_required": pr.network_import_required, + } + + # when: submitting NETWORK step with overridden data + response = client.post(edit_url, wizard_form_network_data, follow=True) + # then: wizard proceeds, CONSENT step is displayed with correct instance/initial data assigned + consent_form = response.context_data["form"] + assert consent_form.instance == pr + assert consent_form.initial == { + "data_processing_opt_in": pr.data_processing_opt_in, + "newsletter_opt_in": pr.newsletter_opt_in, + } + + # when: submitting CONSENT step with overridden data + response = client.post(edit_url, wizard_form_consent, follow=True) + + # then: PREVIEW step is rendered with correct data + preview_form_dict = response.context_data["preview_forms"] + + # org_detail preview displays overridden data + overridden_values = { + "name": wizard_form_org_details_data["0-name"], + "description": wizard_form_org_details_data["0-description"], + "website": wizard_form_org_details_data["0-website"], + } + assert overridden_values.items() <= preview_form_dict["0"].initial.items() + + # locations preview displays overridden data + assert preview_form_dict["1"].forms["locations"].total_form_count() == 3 + + # TODO: expand checking PREVIEW step when injecting the data is fixed + # when: PREVIEW form is submitted + response = client.post(edit_url, wizard_form_preview, follow=True) # then: submitting the final step redirects to the detail view assert response.resolver_match.func.view_class is views.ProviderRequestDetailView + # then: a ProviderRequest object is updated in the db pr_id = response.context_data["providerrequest"].id updated_pr = models.ProviderRequest.objects.get(id=pr_id) - assert updated_pr # TODO: verify fields have changed + assert updated_pr.name == overridden_values["name"] + assert updated_pr.providerrequestlocation_set.count() == 3 From 484ccf188fab54294195868018e987b429138956 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 6 Sep 2023 21:11:55 +0100 Subject: [PATCH 21/30] Rename ProviderRequest status open -> more info required --- ...0061_providerrequest_rename_open_status.py | 38 +++++++++++++++++++ apps/accounts/models/provider_request.py | 2 +- .../provider_portal/request_detail.html | 8 +++- 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 apps/accounts/migrations/0061_providerrequest_rename_open_status.py diff --git a/apps/accounts/migrations/0061_providerrequest_rename_open_status.py b/apps/accounts/migrations/0061_providerrequest_rename_open_status.py new file mode 100644 index 00000000..51bf46fb --- /dev/null +++ b/apps/accounts/migrations/0061_providerrequest_rename_open_status.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.20 on 2023-09-06 19:10 + +from django.db import migrations, models + + +def rename_status(apps, schema_editor): + """ + Rename status OPEN to MORE INFO REQUIRED for existing ProviderRequest objects + """ + db_alias = schema_editor.connection.alias + ProviderRequest = apps.get_model("accounts", "ProviderRequest") + ProviderRequest.objects.using(db_alias).filter(status="Open").update( + status="More info required" + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("accounts", "0060_provider_request_consent"), + ] + + operations = [ + migrations.AlterField( + model_name="providerrequest", + name="status", + field=models.CharField( + choices=[ + ("Pending review", "Pending Review"), + ("Approved", "Approved"), + ("Rejected", "Rejected"), + ("More info required", "Open"), + ("Removed", "Removed"), + ], + max_length=255, + ), + ), + migrations.RunPython(code=rename_status, reverse_code=lambda *args: ...), + ] diff --git a/apps/accounts/models/provider_request.py b/apps/accounts/models/provider_request.py index dab94f6d..44eee626 100644 --- a/apps/accounts/models/provider_request.py +++ b/apps/accounts/models/provider_request.py @@ -40,7 +40,7 @@ class ProviderRequestStatus(models.TextChoices): PENDING_REVIEW = "Pending review" APPROVED = "Approved" REJECTED = "Rejected" - OPEN = "Open" + OPEN = "More info required" REMOVED = "Removed" diff --git a/apps/accounts/templates/provider_portal/request_detail.html b/apps/accounts/templates/provider_portal/request_detail.html index 72e33350..10b8844d 100644 --- a/apps/accounts/templates/provider_portal/request_detail.html +++ b/apps/accounts/templates/provider_portal/request_detail.html @@ -14,12 +14,16 @@

Summary of request

Submitted on: {{ object.created }}

Status: {{ object.status }}

- {% if object.approved_at %} + {% if object.approved_at and object.status|lower == "approved" %}

Approved on: {{ object.approved_at }}

{% endif %} {% if object.status|lower == "pending review" %} - If you want to update the information relating to this provider, please get in touch using our support form. + We are currently reviewing this request. If you want to update the information relating to this provider, please get in touch using our support form. + {% endif %} + + {% if object.status|lower == "more info required" %} +

Update this request

{% endif %} From 440a24a5370a2ca8b37b02ec8dca8f716f169fe8 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Wed, 6 Sep 2023 21:37:33 +0100 Subject: [PATCH 22/30] Graceful handling of the edit view when subresources of a ProviderRequest do not exist --- apps/accounts/tests/test_provider_request.py | 32 ++++++++++-------- apps/accounts/views.py | 35 ++++++++++++++++---- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/apps/accounts/tests/test_provider_request.py b/apps/accounts/tests/test_provider_request.py index bde16a49..fb1dbf5b 100644 --- a/apps/accounts/tests/test_provider_request.py +++ b/apps/accounts/tests/test_provider_request.py @@ -418,7 +418,8 @@ def test_wizard_sends_email_on_submission( assert provider_request.status in msg_body_html -def test_approve_when_hostingprovider_for_user_exists(db, user_with_provider): +@pytest.mark.django_db +def test_approve_when_hostingprovider_for_user_exists(user_with_provider): # given: provider request submitted by a user that already has a Hostingprovider assigned pr = ProviderRequestFactory.create(created_by=user_with_provider) loc1 = ProviderRequestLocationFactory.create(request=pr) @@ -452,7 +453,8 @@ def test_approve_fails_when_request_already_approved(db): pr.approve() -def test_approve_first_location_is_persisted(db): +@pytest.mark.django_db +def test_approve_first_location_is_persisted(): # given: provider request with 2 locations pr = ProviderRequestFactory.create() loc1 = ProviderRequestLocationFactory.create(request=pr) @@ -467,7 +469,8 @@ def test_approve_first_location_is_persisted(db): assert hp.country == loc1.country -def test_approve_asn_already_exists(db, green_asn): +@pytest.mark.django_db +def test_approve_asn_already_exists(green_asn): # given: provider request with ASN that already exists green_asn.save() pr = ProviderRequestFactory.create() @@ -488,7 +491,8 @@ def test_approve_asn_already_exists(db, green_asn): @freeze_time("Apr 25th, 2023, 12:00:01") -def test_approve_changes_status_to_approved(db): +@pytest.mark.django_db +def test_approve_changes_status_to_approved(): # given: a provider request is created pr = ProviderRequestFactory.create() ProviderRequestLocationFactory.create(request=pr) @@ -506,7 +510,8 @@ def test_approve_changes_status_to_approved(db): assert result.approved_at == datetime(2023, 4, 25, 12, 0, 1) -def test_approve_creates_hosting_provider(db): +@pytest.mark.django_db +def test_approve_creates_hosting_provider(): # given: a provider request with services is created pr = ProviderRequestFactory.create(services=faker.words(nb=4)) ProviderRequestLocationFactory.create(request=pr) @@ -536,7 +541,8 @@ def test_approve_creates_hosting_provider(db): assert "other-none" not in hp.services.slugs() -def test_approve_supports_orgs_not_offering_hosted_services(db): +@pytest.mark.django_db +def test_approve_supports_orgs_not_offering_hosted_services(): # given: a verification request for an organisation that does # not offer any services, but we still want ot recognise as green other_none_service = ServiceFactory( @@ -559,7 +565,8 @@ def test_approve_supports_orgs_not_offering_hosted_services(db): assert "other-none" in hp.services.slugs() -def test_approve_creates_ip_ranges(db): +@pytest.mark.django_db +def test_approve_creates_ip_ranges(): # given: a provider request with multiple IP ranges pr = ProviderRequestFactory.create() ProviderRequestLocationFactory.create(request=pr) @@ -580,7 +587,8 @@ def test_approve_creates_ip_ranges(db): ).exists() -def test_approve_creates_asns(db): +@pytest.mark.django_db +def test_approve_creates_asns(): # given: a provider request with multiple locations, IP ranges, ASNs, evidence and consent pr = ProviderRequestFactory.create() ProviderRequestLocationFactory.create(request=pr) @@ -600,7 +608,8 @@ def test_approve_creates_asns(db): @freeze_time("Feb 15th, 2023") -def test_approve_creates_evidence_documents(db): +@pytest.mark.django_db +def test_approve_creates_evidence_documents(): # given: a provider request with multiple locations, IP ranges, ASNs, evidence and consent pr = ProviderRequestFactory.create() ProviderRequestLocationFactory.create(request=pr) @@ -771,7 +780,6 @@ def test_new_submission_doesnt_modify_available_services( def test_edit_view_accessible_by_creator(client): # given: an open provider request pr = ProviderRequestFactory.create() - ProviderRequestLocationFactory.create(request=pr) # when: accessing its edit view by the creator client.force_login(pr.created_by) @@ -786,7 +794,6 @@ def test_edit_view_accessible_by_creator(client): def test_edit_view_accessible_by_admin(client, greenweb_staff_user): # given: an open provider request pr = ProviderRequestFactory.create() - ProviderRequestLocationFactory.create(request=pr) # when: accessing its edit view by Green Web staff client.force_login(greenweb_staff_user) @@ -801,7 +808,6 @@ def test_edit_view_accessible_by_admin(client, greenweb_staff_user): def test_edit_view_inaccessible_by_other_users(client, user): # given: an open provider request pr = ProviderRequestFactory.create() - ProviderRequestLocationFactory.create(request=pr) # when: accessing its edit view by regular users other than the creator client.force_login(user) @@ -825,7 +831,6 @@ def test_edit_view_inaccessible_by_other_users(client, user): def test_edit_view_accessible_for_given_status(client, request_status, status_code): # given: a provider request with a given status pr = ProviderRequestFactory.create(status=request_status) - ProviderRequestLocationFactory.create(request=pr) # when: accessing the edit view by its creator client.force_login(pr.created_by) @@ -840,7 +845,6 @@ def test_edit_view_accessible_for_given_status(client, request_status, status_co def test_edit_view_displays_form_with_prepopulated_data(client): # given: an open provider request pr = ProviderRequestFactory.create() - ProviderRequestLocationFactory.create(request=pr) # when: accessing the edit view by its creator client.force_login(pr.created_by) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index c3061525..8958103f 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -41,7 +41,15 @@ ConsentForm, PreviewForm, ) -from .models import User, ProviderRequest, ProviderRequestStatus, Hostingprovider +from .models import ( + User, + ProviderRequest, + ProviderRequestStatus, + Hostingprovider, + ProviderRequestASN, + ProviderRequestEvidence, + ProviderRequestIPRange, +) from .utils import send_email import logging @@ -478,11 +486,26 @@ def get_instance_dict(self, request_id): except ProviderRequest.DoesNotExist: return {} - # TODO: handle DoesNotExist - location_qs = pr_instance.providerrequestlocation_set.all() - evidence_qs = pr_instance.providerrequestevidence_set.all() - asn_qs = pr_instance.providerrequestasn_set.all() - ip_qs = pr_instance.providerrequestiprange_set.all() + location_qs = ( + pr_instance.providerrequestlocation_set.all() + if pr_instance.providerrequestlocation_set.exists() + else ProviderRequestASN.objects.none() + ) + evidence_qs = ( + pr_instance.providerrequestevidence_set.all() + if pr_instance.providerrequestevidence_set.exists() + else ProviderRequestEvidence.objects.none() + ) + asn_qs = ( + pr_instance.providerrequestasn_set.all() + if pr_instance.providerrequestasn_set.exists() + else ProviderRequestASN.objects.none() + ) + ip_qs = ( + pr_instance.providerrequestiprange_set.all() + if pr_instance.providerrequestiprange_set.exists() + else ProviderRequestIPRange.objects.none() + ) instance_dict = { self.Steps.ORG_DETAILS.value: pr_instance, From 91bafb45929fcf6d266c06311908f9086fa8c2f5 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Thu, 7 Sep 2023 11:15:39 +0100 Subject: [PATCH 23/30] Build instance_dict for ProviderRequestWizardView once and inject it in the as_view method in urls --- apps/accounts/urls.py | 16 +++++--- apps/accounts/views.py | 86 ++++++++++++++++++++---------------------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py index 9b31c4c9..1de09cd1 100644 --- a/apps/accounts/urls.py +++ b/apps/accounts/urls.py @@ -115,16 +115,22 @@ ProviderRequestDetailView.as_view(), name="provider_request_detail", ), - path( - "requests//edit/", - ProviderRequestWizardView.as_view(ProviderRequestWizardView.FORMS), - name="provider_request_edit", - ), path( "requests/new/", ProviderRequestWizardView.as_view(ProviderRequestWizardView.FORMS), name="provider_registration", ), + # For editing verification requests, we use the same view as for adding new ones + # and pass instance_dict as a parameter to inject model instances to forms. + # See more info: https://django-formtools.readthedocs.io/en/stable/wizard.html#formtools.wizard.views.WizardView.instance_dict + path( + "requests//edit/", + lambda request, request_id: ProviderRequestWizardView.as_view( + ProviderRequestWizardView.FORMS, + instance_dict=ProviderRequestWizardView.get_instance_dict(request_id), + )(request, request_id=request_id), + name="provider_request_edit", + ), path( "provider-autocomplete/", ProviderAutocompleteView.as_view(), diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 8958103f..37b39d87 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -476,7 +476,40 @@ def get_form_kwargs(self, step=None): return {"instance": self.get_form_instance(step)} return {} - def get_instance_dict(self, request_id): + def _send_notification_email(self, provider_request: ProviderRequest): + """ + Send notification to support staff, and the user to acknowledge their submission. + """ # noqa + + current_site = get_current_site(self.request) + connection_scheme = self.request.scheme + user = self.request.user + request_path = reverse("provider_request_detail", args=[provider_request.id]) + + link_to_verification_request = ( + f"{connection_scheme}://{current_site.domain}{request_path}" + ) + + ctx = { + "org_name": provider_request.name, + "status": provider_request.status, + "link_to_verification_request": link_to_verification_request, + } + + send_email( + address=user.email, + subject=( + f"Your verification request for the Green Web Database: " + f"{mark_safe(provider_request.name)}" + ), + context=ctx, + template_html="emails/verification-request-notify.html", + template_txt="emails/verification-request-notify.txt", + bcc=settings.TRELLO_REGISTRATION_EMAIL_TO_BOARD_ADDRESS, + ) + + @classmethod + def get_instance_dict(cls, request_id): """ Based on request_id, return existing instances of ProviderRequest and related objects in a map that matches the structure of the forms. @@ -508,57 +541,18 @@ def get_instance_dict(self, request_id): ) instance_dict = { - self.Steps.ORG_DETAILS.value: pr_instance, - self.Steps.LOCATIONS.value: { + cls.Steps.ORG_DETAILS.value: pr_instance, + cls.Steps.LOCATIONS.value: { "locations": location_qs, "extra": pr_instance, }, - self.Steps.SERVICES.value: pr_instance, - self.Steps.GREEN_EVIDENCE.value: evidence_qs, - self.Steps.NETWORK_FOOTPRINT.value: { + cls.Steps.SERVICES.value: pr_instance, + cls.Steps.GREEN_EVIDENCE.value: evidence_qs, + cls.Steps.NETWORK_FOOTPRINT.value: { "ips": ip_qs, "asns": asn_qs, "extra": pr_instance, }, - self.Steps.CONSENT.value: pr_instance, + cls.Steps.CONSENT.value: pr_instance, } return instance_dict - - def get_form_instance(self, step): - # TODO: optimize this - do not construct instance_dict on every call - request_id = self.kwargs.get("request_id") - if not request_id: - return None - return self.get_instance_dict(request_id)[step] - - def _send_notification_email(self, provider_request: ProviderRequest): - """ - Send notification to support staff, and the user to acknowledge their submission. - """ # noqa - - current_site = get_current_site(self.request) - connection_scheme = self.request.scheme - user = self.request.user - request_path = reverse("provider_request_detail", args=[provider_request.id]) - - link_to_verification_request = ( - f"{connection_scheme}://{current_site.domain}{request_path}" - ) - - ctx = { - "org_name": provider_request.name, - "status": provider_request.status, - "link_to_verification_request": link_to_verification_request, - } - - send_email( - address=user.email, - subject=( - f"Your verification request for the Green Web Database: " - f"{mark_safe(provider_request.name)}" - ), - context=ctx, - template_html="emails/verification-request-notify.html", - template_txt="emails/verification-request-notify.txt", - bcc=settings.TRELLO_REGISTRATION_EMAIL_TO_BOARD_ADDRESS, - ) From b2ea0fe04427adbc79ea67fc81718cf1dcbf8d11 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Fri, 8 Sep 2023 00:37:34 +0100 Subject: [PATCH 24/30] =?UTF-8?q?=F0=9F=99=8F=20Fix=20the=20preview=20rend?= =?UTF-8?q?ering=20for=20ModelFormSets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/forms.py | 16 ++++++++++++++++ .../provider_registration/partials/_preview.html | 2 +- .../templates/provider_registration/preview.html | 8 ++++---- apps/accounts/templatetags/preview_extras.py | 5 +++++ apps/accounts/tests/test_provider_request.py | 2 +- apps/accounts/views.py | 12 ++++-------- 6 files changed, 31 insertions(+), 14 deletions(-) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 2c7991a5..4cc69c7d 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -307,6 +307,22 @@ class Meta: class MoreConvenientFormset(ConvenientBaseModelFormSet): + def __init__(self, *args, **kwargs): + """ + Override a whacky ModelFormSets behavior in order to + render initial forms correctly (important for the preview step!) + + Problem: ModelFormSets take "initial" argument to populate initial forms, + but the value is truncated to the number of "extra" forms + as configured in the modelformset_factory. + + Docs: https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#id2 + """ + initial = kwargs.get("initial") + if initial: + self.extra = len(initial) - 1 + super().__init__(*args, **kwargs) + def get_queryset(self): """ Built-in BaseModelFormSet uses model's default manager get_queryset, diff --git a/apps/accounts/templates/provider_registration/partials/_preview.html b/apps/accounts/templates/provider_registration/partials/_preview.html index 66633671..47f103fc 100644 --- a/apps/accounts/templates/provider_registration/partials/_preview.html +++ b/apps/accounts/templates/provider_registration/partials/_preview.html @@ -2,7 +2,7 @@ {% load countries %}
- {% for field in form %} + {% for field in form|exclude_id_fields %}

{{ field.label }}

{% if field.label|lower == "which services does your organisation offer?" %} diff --git a/apps/accounts/templates/provider_registration/preview.html b/apps/accounts/templates/provider_registration/preview.html index 27ec08b9..759aa4c2 100644 --- a/apps/accounts/templates/provider_registration/preview.html +++ b/apps/accounts/templates/provider_registration/preview.html @@ -40,7 +40,7 @@

About your organisation

Locations

- {% for location_form in preview_forms.1.locations.initial_forms %} + {% for location_form in preview_forms.1.locations.forms %} {% include "provider_registration/partials/_preview.html" with form=location_form %} {% endfor %} {% include "provider_registration/partials/_preview.html" with form=preview_forms.1.extra %} @@ -53,7 +53,7 @@

Services provided by your organisation

Green evidence

- {% for evidence_form in preview_forms.3.initial_forms %} + {% for evidence_form in preview_forms.3.forms %} {% include "provider_registration/partials/_preview.html" with form=evidence_form %} {% endfor %}
@@ -61,7 +61,7 @@

Green evidence

Network footprint

- {% for ip_form in preview_forms.4.ips.initial_forms %} + {% for ip_form in preview_forms.4.ips.forms %}

IP range

@@ -70,7 +70,7 @@

Network footprint

{% endfor %} - {% for asn_form in preview_forms.4.asns.initial_forms %} + {% for asn_form in preview_forms.4.asns.forms %} {% include "provider_registration/partials/_preview.html" with form=asn_form %} {% endfor %} diff --git a/apps/accounts/templatetags/preview_extras.py b/apps/accounts/templatetags/preview_extras.py index 897149ef..076381eb 100644 --- a/apps/accounts/templatetags/preview_extras.py +++ b/apps/accounts/templatetags/preview_extras.py @@ -29,3 +29,8 @@ def render_as_services(value): if tags: return ", ".join([tag.name for tag in tags]) return None + + +@register.filter +def exclude_id_fields(form): + return [field for field in form if field.label.lower() != "id"] diff --git a/apps/accounts/tests/test_provider_request.py b/apps/accounts/tests/test_provider_request.py index fb1dbf5b..c460a8b8 100644 --- a/apps/accounts/tests/test_provider_request.py +++ b/apps/accounts/tests/test_provider_request.py @@ -1010,7 +1010,7 @@ def test_editing_pr_updates_original_submission( assert overridden_values.items() <= preview_form_dict["0"].initial.items() # locations preview displays overridden data - assert preview_form_dict["1"].forms["locations"].total_form_count() == 3 + assert len(preview_form_dict["1"].forms["locations"].forms) == 3 # TODO: expand checking PREVIEW step when injecting the data is fixed # when: PREVIEW form is submitted diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 37b39d87..a7dcef61 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -439,15 +439,11 @@ def _get_data_for_preview(self): - The forms are not bound: `form.data` will return {}. The initial data should be accessed through iterating over bound fields: `field.value for field in form`. - - - In case of a formset instance, accessing `formset.forms` - will include extra (empty) forms as defined per formset factory. - Templates should use `formset.initial_forms` for rendering non-empty forms. + - Iterating over the fields like mentioned above will also include + the "id" field for ModelForms and ModelFormSets, that's why in the templates + it's recommended to use the the template tag "exclude_id_fields". """ - # TODO: fix passing data for the preview step - # PROBLEM: ModelFormSets take initial value truncated to extra forms only! - # that's why we only see 1 instance passed to the formset - # https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#id2 + preview_forms = {} # iterate over all forms without the last one (PREVIEW) for step, form in self.FORMS[:-1]: From 396f397dffc119aca3fc749e5194ed7cf8d7b9e8 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Thu, 21 Sep 2023 01:17:36 +0200 Subject: [PATCH 25/30] Fix deleting forms --- apps/accounts/forms.py | 10 ++++++ .../provider_registration/evidence.html | 7 ++++- .../partials/_preview.html | 2 +- .../provider_registration/preview.html | 28 +++++++++++------ apps/accounts/templatetags/preview_extras.py | 7 +++-- apps/accounts/views.py | 31 ++++++++++++------- 6 files changed, 59 insertions(+), 26 deletions(-) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 4cc69c7d..2d6f6c40 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -329,6 +329,8 @@ def get_queryset(self): which returns all objects. As a consequence the formset would display all available objects. + Docs: https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#changing-the-queryset + We change that behavior so that unless a "queryset" parameter is passed to the formset (i.e. in editing mode), empty queryset should be returned. @@ -380,6 +382,8 @@ class GreenEvidenceForm( formset=MoreConvenientFormset, validate_min=True, min_num=1, + can_delete=True, + can_delete_extra=True, ) ): def non_form_errors(self): @@ -415,12 +419,16 @@ class Meta: form=IpRangeForm, formset=MoreConvenientFormset, extra=0, + can_delete=True, + can_delete_extra=True, ) AsnFormset = forms.modelformset_factory( model=ac_models.ProviderRequestASN, form=AsnForm, formset=MoreConvenientFormset, extra=0, + can_delete=True, + can_delete_extra=True, ) @@ -615,6 +623,8 @@ class Meta: formset=MoreConvenientFormset, validate_min=True, min_num=1, + can_delete=True, + can_delete_extra=True, ) diff --git a/apps/accounts/templates/provider_registration/evidence.html b/apps/accounts/templates/provider_registration/evidence.html index 262f029b..b561b4c4 100644 --- a/apps/accounts/templates/provider_registration/evidence.html +++ b/apps/accounts/templates/provider_registration/evidence.html @@ -24,6 +24,11 @@ }); }); +
@@ -56,7 +61,7 @@

Submit your evidence

{% endcomment %} {% if wizard.form.non_form_errors %}
-
+
{{ wizard.form.non_form_errors}} diff --git a/apps/accounts/templates/provider_registration/partials/_preview.html b/apps/accounts/templates/provider_registration/partials/_preview.html index 47f103fc..133a86f2 100644 --- a/apps/accounts/templates/provider_registration/partials/_preview.html +++ b/apps/accounts/templates/provider_registration/partials/_preview.html @@ -2,7 +2,7 @@ {% load countries %}
- {% for field in form|exclude_id_fields %} + {% for field in form|exclude_preview_fields %}

{{ field.label }}

{% if field.label|lower == "which services does your organisation offer?" %} diff --git a/apps/accounts/templates/provider_registration/preview.html b/apps/accounts/templates/provider_registration/preview.html index 759aa4c2..23b40e15 100644 --- a/apps/accounts/templates/provider_registration/preview.html +++ b/apps/accounts/templates/provider_registration/preview.html @@ -41,7 +41,9 @@

About your organisation

Locations

{% for location_form in preview_forms.1.locations.forms %} - {% include "provider_registration/partials/_preview.html" with form=location_form %} + {% if not location_form.DELETE.value %} + {% include "provider_registration/partials/_preview.html" with form=location_form %} + {% endif %} {% endfor %} {% include "provider_registration/partials/_preview.html" with form=preview_forms.1.extra %}
@@ -54,24 +56,30 @@

Services provided by your organisation

Green evidence

{% for evidence_form in preview_forms.3.forms %} - {% include "provider_registration/partials/_preview.html" with form=evidence_form %} + {% if not evidence_form.DELETE.value %} + {% include "provider_registration/partials/_preview.html" with form=evidence_form %} + {% endif %} {% endfor %}

Network footprint

- {% for ip_form in preview_forms.4.ips.forms %} -
-
-

IP range

-

{{ ip_form.start.value }} - {{ ip_form.end.value }}

+ {% for ip_form in preview_forms.4.ips %} + {% if not ip_form.DELETE.value %} +
+
+

IP range

+

{{ ip_form.start.value }} - {{ ip_form.end.value }}

+
-
+ {% endif %} {% endfor %} - {% for asn_form in preview_forms.4.asns.forms %} - {% include "provider_registration/partials/_preview.html" with form=asn_form %} + {% for asn_form in preview_forms.4.asns %} + {% if not asn_form.DELETE.value %} + {% include "provider_registration/partials/_preview.html" with form=asn_form %} + {% endif %} {% endfor %} {% include "provider_registration/partials/_preview.html" with form=preview_forms.4.extra %} diff --git a/apps/accounts/templatetags/preview_extras.py b/apps/accounts/templatetags/preview_extras.py index 076381eb..f1ec080b 100644 --- a/apps/accounts/templatetags/preview_extras.py +++ b/apps/accounts/templatetags/preview_extras.py @@ -32,5 +32,8 @@ def render_as_services(value): @register.filter -def exclude_id_fields(form): - return [field for field in form if field.label.lower() != "id"] +def exclude_preview_fields(form): + """ + On preview, exclude fields "id" and "delete" from forms + """ + return [field for field in form if field.label.lower() not in ["id", "delete"]] diff --git a/apps/accounts/views.py b/apps/accounts/views.py index a7dcef61..bb1570bf 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -334,10 +334,12 @@ def done(self, form_list, form_dict, **kwargs): # process LOCATIONS form: extract locations locations_formset = form_dict[steps.LOCATIONS.value].forms["locations"] - for location_form in locations_formset: - location = location_form.save(commit=False) + locations = locations_formset.save(commit=False) + for location in locations: location.request = pr location.save() + for object_to_delete in locations_formset.deleted_objects: + object_to_delete.delete() # process LOCATION: check if a bulk location import is needed extra_location_form = form_dict[steps.LOCATIONS.value].forms["extra"] @@ -355,25 +357,31 @@ def done(self, form_list, form_dict, **kwargs): pr.save() # process GREEN_EVIDENCE form: link evidence to ProviderRequest - evidence_forms = form_dict[steps.GREEN_EVIDENCE.value].forms - for evidence_form in evidence_forms: - evidence = evidence_form.save(commit=False) + evidence_formset = form_dict[steps.GREEN_EVIDENCE.value] + evidence_instances = evidence_formset.save(commit=False) + for evidence in evidence_instances: evidence.request = pr evidence.save() + for object_to_delete in evidence_formset.deleted_objects: + object_to_delete.delete() # process NETWORK_FOOTPRINT form: retrieve IP ranges - ip_range_forms = form_dict[steps.NETWORK_FOOTPRINT.value].forms["ips"] - for ip_range_form in ip_range_forms: - ip_range = ip_range_form.save(commit=False) + ip_range_formset = form_dict[steps.NETWORK_FOOTPRINT.value].forms["ips"] + ip_range_instances = ip_range_formset.save(commit=False) + for ip_range in ip_range_instances: ip_range.request = pr ip_range.save() + for object_to_delete in ip_range_formset.deleted_objects: + object_to_delete.delete() # process NETWORK_FOOTPRINT form: retrieve ASNs - asn_forms = form_dict[steps.NETWORK_FOOTPRINT.value].forms["asns"] - for asn_form in asn_forms: - asn = asn_form.save(commit=False) + asn_formset = form_dict[steps.NETWORK_FOOTPRINT.value].forms["asns"] + asn_instances = asn_formset.save(commit=False) + for asn in asn_instances: asn.request = pr asn.save() + for object_to_delete in asn_formset.deleted_objects: + object_to_delete.delete() # process NETWORK_FOOTPRINT form: retrieve network explanation # if network data is missing @@ -449,7 +457,6 @@ def _get_data_for_preview(self): for step, form in self.FORMS[:-1]: cleaned_data = self.get_cleaned_data_for_step(step) preview_forms[step] = form(initial=cleaned_data) - return preview_forms def get_context_data(self, form, **kwargs): From ac184f91302392a3ecae83dde8eed30a0bb98df7 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Thu, 21 Sep 2023 02:18:50 +0200 Subject: [PATCH 26/30] Fix preview: correct number of forms in a formset --- apps/accounts/forms.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 2d6f6c40..9e31aefa 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -312,15 +312,30 @@ def __init__(self, *args, **kwargs): Override a whacky ModelFormSets behavior in order to render initial forms correctly (important for the preview step!) - Problem: ModelFormSets take "initial" argument to populate initial forms, + Problem #1: ModelFormSets take "initial" argument to populate initial forms, but the value is truncated to the number of "extra" forms - as configured in the modelformset_factory. + as configured in the modelformset_factory. Example: + + MyModelFormset.extra = 1 + formset = MyModelFormset(initial=[data1, data2, data3]) + formset.forms == 1 # and with this fix it's 3 as we'd expect + + Problem #2: FormSets that have min_num argument passed greater than 0 + (requiring at least 1 form to be submitted) have some internal magic that + manipulate the number of extra forms based on that. For those the value + needs to be decreased to acommodate for that. Otherwise the preview step + displays empty extra forms. Example: + + MyModelFormset.extra = 1 + MyModelFormset.min_num = 1 + formset = MyModelFormset(initial=[data1]) + formset.forms == 2 # and with this fix it's 1 as we'd expect Docs: https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#id2 """ initial = kwargs.get("initial") if initial: - self.extra = len(initial) - 1 + self.extra = len(initial) - self.min_num super().__init__(*args, **kwargs) def get_queryset(self): From f20edf00d216d2c96049e582bec0e7fd2c282417 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Thu, 21 Sep 2023 02:19:08 +0200 Subject: [PATCH 27/30] Deduplicate code for processing formsets --- apps/accounts/views.py | 42 ++++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index bb1570bf..26c2783a 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -325,6 +325,15 @@ def done(self, form_list, form_dict, **kwargs): Reference: https://django-formtools.readthedocs.io/en/latest/wizard.html#formtools.wizard.views.WizardView.done """ # noqa + + def _process_formset(formset, request): + instances = formset.save(commit=False) + for instance in instances: + instance.request = request + instance.save() + for object_to_delete in formset.deleted_objects: + object_to_delete.delete() + steps = ProviderRequestWizardView.Steps # process ORG_DETAILS form: extract ProviderRequest and Location @@ -334,12 +343,7 @@ def done(self, form_list, form_dict, **kwargs): # process LOCATIONS form: extract locations locations_formset = form_dict[steps.LOCATIONS.value].forms["locations"] - locations = locations_formset.save(commit=False) - for location in locations: - location.request = pr - location.save() - for object_to_delete in locations_formset.deleted_objects: - object_to_delete.delete() + _process_formset(locations_formset, pr) # process LOCATION: check if a bulk location import is needed extra_location_form = form_dict[steps.LOCATIONS.value].forms["extra"] @@ -358,30 +362,15 @@ def done(self, form_list, form_dict, **kwargs): # process GREEN_EVIDENCE form: link evidence to ProviderRequest evidence_formset = form_dict[steps.GREEN_EVIDENCE.value] - evidence_instances = evidence_formset.save(commit=False) - for evidence in evidence_instances: - evidence.request = pr - evidence.save() - for object_to_delete in evidence_formset.deleted_objects: - object_to_delete.delete() + _process_formset(evidence_formset, pr) # process NETWORK_FOOTPRINT form: retrieve IP ranges ip_range_formset = form_dict[steps.NETWORK_FOOTPRINT.value].forms["ips"] - ip_range_instances = ip_range_formset.save(commit=False) - for ip_range in ip_range_instances: - ip_range.request = pr - ip_range.save() - for object_to_delete in ip_range_formset.deleted_objects: - object_to_delete.delete() + _process_formset(ip_range_formset, pr) # process NETWORK_FOOTPRINT form: retrieve ASNs asn_formset = form_dict[steps.NETWORK_FOOTPRINT.value].forms["asns"] - asn_instances = asn_formset.save(commit=False) - for asn in asn_instances: - asn.request = pr - asn.save() - for object_to_delete in asn_formset.deleted_objects: - object_to_delete.delete() + _process_formset(asn_formset, pr) # process NETWORK_FOOTPRINT form: retrieve network explanation # if network data is missing @@ -448,8 +437,9 @@ def _get_data_for_preview(self): The initial data should be accessed through iterating over bound fields: `field.value for field in form`. - Iterating over the fields like mentioned above will also include - the "id" field for ModelForms and ModelFormSets, that's why in the templates - it's recommended to use the the template tag "exclude_id_fields". + the "id" field for ModelForms and ModelFormSets, as well as "DELETE" field + to mark deleted forms in the formsets. To render forms without these fields in the templates + it's recommended to use the the template tag "exclude_preview_fields". """ preview_forms = {} From f14318f353705e2a2041fdf5989c71686bc076ec Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Thu, 21 Sep 2023 13:20:49 +0200 Subject: [PATCH 28/30] Fix test for overriding modelformset data --- apps/accounts/tests/test_provider_request.py | 29 ++++++++++++++++++-- apps/accounts/views.py | 2 ++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/apps/accounts/tests/test_provider_request.py b/apps/accounts/tests/test_provider_request.py index c460a8b8..b4637d9a 100644 --- a/apps/accounts/tests/test_provider_request.py +++ b/apps/accounts/tests/test_provider_request.py @@ -869,7 +869,6 @@ def test_edit_view_displays_form_with_prepopulated_data(client): def test_editing_pr_updates_original_submission( client, wizard_form_org_details_data, - wizard_form_org_location_data, wizard_form_services_data, wizard_form_evidence_data, wizard_form_network_data, @@ -933,7 +932,29 @@ def test_editing_pr_updates_original_submission( "city": loc2.city, "country": loc2.country, } + # when: submitting LOCATIONS form with overridden data + # data to override locations: delete existing 2 locations, add 3 new ones + wizard_form_org_location_data = { + "provider_request_wizard_view-current_step": "1", + "locations__1-TOTAL_FORMS": "5", + "locations__1-INITIAL_FORMS": "2", + "locations__1-0-country": loc1.country.code, + "locations__1-0-city": loc1.city, + "locations__1-0-id": str(loc1.id), + "locations__1-0-DELETE": "on", + "locations__1-1-country": loc2.country.code, + "locations__1-1-city": loc2.city, + "locations__1-1-id": str(loc2.id), + "locations__1-1-DELETE": "on", + "locations__1-2-country": faker.country_code(), + "locations__1-2-city": faker.city(), + "locations__1-3-country": faker.country_code(), + "locations__1-3-city": faker.city(), + "locations__1-4-country": faker.country_code(), + "locations__1-4-city": faker.city(), + "extra__1-location_import_required": "True", + } response = client.post(edit_url, wizard_form_org_location_data, follow=True) # then: wizard proceeds, SERVICES form is displayed with bound instance and initial data @@ -1010,9 +1031,11 @@ def test_editing_pr_updates_original_submission( assert overridden_values.items() <= preview_form_dict["0"].initial.items() # locations preview displays overridden data - assert len(preview_form_dict["1"].forms["locations"].forms) == 3 + location_forms = preview_form_dict["1"].forms["locations"].forms + # 5 forms in total are passed, 3 of them not marked as deleted + assert len(location_forms) == 5 + assert len([form for form in location_forms if not form["DELETE"].value()]) == 3 - # TODO: expand checking PREVIEW step when injecting the data is fixed # when: PREVIEW form is submitted response = client.post(edit_url, wizard_form_preview, follow=True) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 26c2783a..6fe14f25 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -440,6 +440,8 @@ def _get_data_for_preview(self): the "id" field for ModelForms and ModelFormSets, as well as "DELETE" field to mark deleted forms in the formsets. To render forms without these fields in the templates it's recommended to use the the template tag "exclude_preview_fields". + - Forms marked for deletion are also passed to the preview step, that's why + it's necessary to filter them out in the template (based on the value of the DELETE field). """ preview_forms = {} From 926b589d0cd215cec6feb406855c1e8ae334034c Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Thu, 21 Sep 2023 13:55:07 +0200 Subject: [PATCH 29/30] Update Pipfile.lock to keep the build green --- Pipfile | 4 +- Pipfile.lock | 571 +++++++++++++++++++++++++++------------------------ 2 files changed, 299 insertions(+), 276 deletions(-) diff --git a/Pipfile b/Pipfile index 09ffa752..340ae882 100644 --- a/Pipfile +++ b/Pipfile @@ -86,10 +86,10 @@ python-dateutil = "*" geoip2 = "*" iso3166 = "*" django-logentry-admin = "*" -pandas = "*" +pandas = "~=2.0.3" rich = "*" duckdb = "*" -numpy = "*" +numpy = "~=1.24.4" dateutils = "*" freezegun = "*" django-filter = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 466523bd..96a10760 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "291cca29d51fe464e188ff168a929001b92229b69522e167895f06a20adf17b2" + "sha256": "b4fa4b7a28d74f950ddf3ca7303291a6f18ad9a501869cb02ef881a6295ee7f9" }, "pipfile-spec": 6, "requires": {}, @@ -141,11 +141,11 @@ }, "awscli": { "hashes": [ - "sha256:20868edf4ef0b7f368ef25668853f7f70d1a7412f057ebee022c0876647ea952", - "sha256:de1bdb6864018fe9db19293073538db82c6f469bbb471f088dc1fb331b2344e8" + "sha256:211b6703d8dfb696ab3d8199a94a227819a62bed6337f3caab7339099a4475a8", + "sha256:dcbf7d67d1b89069bf287f422a7a62b7747773377f04de2bb4105c41977119e5" ], "index": "pypi", - "version": "==1.29.37" + "version": "==1.29.52" }, "awscli-plugin-endpoint": { "hashes": [ @@ -191,19 +191,19 @@ }, "boto3": { "hashes": [ - "sha256:4aec1b54ba6cd352abba2cdd7cdc76e631a4d3ce79c55c0719f85f9c9842e4a2", - "sha256:709cf438ad3ea48d426e4659538fe1148fc2719469b52179d07a11c5d26abac6" + "sha256:1d36db102517d62c6968b3b0636303241f56859d12dd071def4882fc6e030b20", + "sha256:a34fc153cb2f6fb2f79a764286c967392e8aae9412381d943bddc576c4f7631a" ], "index": "pypi", - "version": "==1.28.37" + "version": "==1.28.52" }, "botocore": { "hashes": [ - "sha256:5c92c8bc3c6b49950c95501b30f0ac551fd4952359b53a6fba243094028157de", - "sha256:72e10759be3dff39c5eeb29f85c11a227c369c946d044f2caf62c352d6a6fc06" + "sha256:46b0a75a38521aa6a75fddccb1542e002930e609d4e13516f40fef170d32e515", + "sha256:6d09881c5a8be34b497872ca3936f8757d886a6f42f2a8703411928189cfedc0" ], "markers": "python_version >= '3.7'", - "version": "==1.31.37" + "version": "==1.31.52" }, "brotli": { "hashes": [ @@ -1163,34 +1163,37 @@ }, "numpy": { "hashes": [ - "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2", - "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55", - "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf", - "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01", - "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca", - "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901", - "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d", - "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4", - "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf", - "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380", - "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044", - "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545", - "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f", - "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f", - "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3", - "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364", - "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9", - "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418", - "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f", - "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295", - "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3", - "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187", - "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926", - "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357", - "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760" - ], - "index": "pypi", - "version": "==1.25.2" + "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f", + "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61", + "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7", + "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400", + "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef", + "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2", + "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d", + "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc", + "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835", + "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706", + "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5", + "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4", + "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6", + "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463", + "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a", + "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f", + "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e", + "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e", + "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694", + "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8", + "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64", + "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d", + "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc", + "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254", + "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2", + "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1", + "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810", + "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9" + ], + "index": "pypi", + "version": "==1.24.4" }, "packaging": { "hashes": [ @@ -1418,11 +1421,11 @@ }, "rich": { "hashes": [ - "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808", - "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39" + "sha256:87b43e0543149efa1253f485cd845bb7ee54df16c9617b8a893650ab84b4acb6", + "sha256:9257b468badc3d347e146a4faa268ff229039d4c2d176ab0cffb4c4fbc73d5d9" ], "index": "pypi", - "version": "==13.5.2" + "version": "==13.5.3" }, "rsa": { "hashes": [ @@ -1493,11 +1496,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:2e53ad63f96bb9da6570ba2e755c267e529edcf58580a2c0d2a11ef26e1e678b", - "sha256:7dc873b87e1faf4d00614afd1058bfa1522942f33daef8a59f90de8ed75cd10c" + "sha256:64a7141005fb775b9db298a30de93e3b83e0ddd1232dc6f36eb38aebc1553291", + "sha256:6de2e88304873484207fed836388e422aeff000609b104c802749fd89d56ba5b" ], "index": "pypi", - "version": "==1.30.0" + "version": "==1.31.0" }, "six": { "hashes": [ @@ -1560,11 +1563,11 @@ }, "sqlite-utils": { "hashes": [ - "sha256:8f6fe7f8d12772cd5cf4594703a98dcd0c37c0fd6820dd20541ba74b9fca363a", - "sha256:d95591db18716ce7eac9a6359f4958598944d9e6640ccc42a2be6ab8456edddf" + "sha256:58da19f64b37fd47e33158ac4dadf2616701cd17d825a1625866d04647f72805", + "sha256:e0f03e6976b05bdb7a5c56454971a0e980fc16dbfd3512bbd3bdcac4f0e4370e" ], "index": "pypi", - "version": "==3.35" + "version": "==3.35.1" }, "sqlparse": { "hashes": [ @@ -1752,37 +1755,21 @@ "markers": "python_version >= '3.6'", "version": "==0.7.13" }, - "ansible-compat": { - "hashes": [ - "sha256:7560e511a660e286c4e8777d82e25990526e45601c37b872c1b64a62126239a5", - "sha256:f58135f5d123e08fdb7e11849b82945e1900f8c899ba0859d4b41b25c76ca955" - ], - "markers": "python_version >= '3.9'", - "version": "==4.1.8" - }, "ansible-core": { "hashes": [ - "sha256:261bc01a15274fc5a6950d5b92b9aa1b7d7c6e8f7543c914505e5bfd9744793a", - "sha256:bc2f5ab74e1c81609aaa9bc8f7f92d939d8e1c847923290301231bdf4dadc812" + "sha256:51ce7c363e619e82e9b8cd3af72ffe75a9567a92687b9cc639dba0cfd30397f3", + "sha256:5923c586dbb92b3661aaa4d70a2ccc2216db21079c6f9b517f05f4f876934be4" ], - "markers": "python_version >= '3.9'", - "version": "==2.15.3" + "markers": "python_version < '3.9'", + "version": "==2.13.12" }, "ansible-lint": { "hashes": [ - "sha256:2c5dc2553604503cf46af91226f92394849833bf20bd4d3a170866e1c3a02779", - "sha256:54744ee7f8fd0ec38051f0b6df2153523939a391ee4bb48f0885b5fcdd82f9b9" + "sha256:419b1a2c8523fbe0c0e8a6c51a5c41a692badb5a530e880a9050ac3a69c3fde3", + "sha256:435c12b4fd88da815af6821f3bf8b04ebb651811da89a11c9d190baff21badaa" ], "index": "pypi", - "version": "==6.18.0" - }, - "appnope": { - "hashes": [ - "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24", - "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e" - ], - "markers": "sys_platform == 'darwin'", - "version": "==0.1.3" + "version": "==6.13.1" }, "appnope": { "hashes": [ @@ -1838,6 +1825,28 @@ ], "version": "==0.2.0" }, + "backports.zoneinfo": { + "hashes": [ + "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf", + "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328", + "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546", + "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6", + "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570", + "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9", + "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7", + "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987", + "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722", + "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582", + "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc", + "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b", + "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1", + "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08", + "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac", + "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2" + ], + "markers": "python_version < '3.9'", + "version": "==0.2.1" + }, "beautifulsoup4": { "hashes": [ "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da", @@ -2057,92 +2066,91 @@ "version": "==0.4.4" }, "coverage": { - "extras": [], - "hashes": [ - "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34", - "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e", - "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7", - "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b", - "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3", - "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985", - "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95", - "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2", - "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a", - "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74", - "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd", - "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af", - "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54", - "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865", - "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214", - "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54", - "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe", - "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0", - "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321", - "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446", - "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e", - "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527", - "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12", - "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f", - "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f", - "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84", - "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479", - "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e", - "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873", - "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70", - "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0", - "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977", - "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51", - "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28", - "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1", - "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254", - "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1", - "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd", - "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689", - "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d", - "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543", - "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9", - "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637", - "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071", - "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482", - "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1", - "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b", - "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5", - "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a", - "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393", - "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a", - "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba" - ], - "index": "pypi", - "version": "==7.3.0" + "hashes": [ + "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375", + "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344", + "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e", + "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745", + "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f", + "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194", + "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a", + "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f", + "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760", + "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8", + "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392", + "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d", + "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc", + "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40", + "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981", + "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0", + "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92", + "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3", + "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0", + "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086", + "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7", + "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465", + "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140", + "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952", + "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3", + "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8", + "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f", + "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593", + "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0", + "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204", + "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037", + "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276", + "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9", + "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26", + "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce", + "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7", + "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136", + "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a", + "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4", + "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c", + "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f", + "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832", + "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3", + "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969", + "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520", + "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887", + "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3", + "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6", + "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1", + "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff", + "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981", + "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e" + ], + "index": "pypi", + "version": "==7.3.1" }, "cryptography": { "hashes": [ - "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306", - "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84", - "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47", - "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d", - "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116", - "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207", - "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81", - "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087", - "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd", - "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507", - "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858", - "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae", - "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34", - "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906", - "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd", - "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922", - "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7", - "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4", - "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574", - "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1", - "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c", - "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e", - "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de" + "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67", + "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311", + "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8", + "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13", + "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143", + "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f", + "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829", + "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd", + "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397", + "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac", + "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d", + "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a", + "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839", + "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e", + "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6", + "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9", + "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860", + "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca", + "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91", + "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d", + "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714", + "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb", + "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f" ], "markers": "python_version >= '3.7'", - "version": "==41.0.3" + "version": "==41.0.4" }, "decorator": { "hashes": [ @@ -2228,19 +2236,19 @@ }, "faker": { "hashes": [ - "sha256:a6624d9574623bb27dfca33fff94581cd7b23b562901db8ad59acbde9a52543e", - "sha256:e2722fdf622cf24e974aaba15a3dee97a6f8b98d869bd827ff1af9c87695af46" + "sha256:8fba91068dc26e3159c1ac9f22444a2338704b0991d86605322e454bda420092", + "sha256:d5d5953556b0fb428a46019e03fc2d40eab2980135ddef5a9eb3d054947fdf83" ], "index": "pypi", - "version": "==19.3.1" + "version": "==19.6.2" }, "filelock": { "hashes": [ - "sha256:0ecc1dd2ec4672a10c8550a8182f1bd0c0a5088470ecd5a125e45f49472fac3d", - "sha256:f067e40ccc40f2b48395a80fcbd4728262fab54e232e090a4063ab804179efeb" + "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4", + "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd" ], "markers": "python_version >= '3.8'", - "version": "==3.12.3" + "version": "==3.12.4" }, "flake8": { "hashes": [ @@ -2260,11 +2268,11 @@ }, "hypothesis": { "hashes": [ - "sha256:06069ff2f18b530a253c0b853b9fae299369cf8f025b3ad3b86ee7131ecd3207", - "sha256:7950944b4a8b7610ab32d077a05e48bec30ecee7385e4d75eedd8120974b199e" + "sha256:e1d36522824d62bb3e9fcb7b57dd4a6ca330bb36921324bb19c476bdafabeda7", + "sha256:e5d75d70f5a4fc372cddf03ec6141237a0a270ed106aeb2156a4984f06d37b0f" ], "index": "pypi", - "version": "==6.82.7" + "version": "==6.86.2" }, "idna": { "hashes": [ @@ -2292,11 +2300,11 @@ }, "importlib-resources": { "hashes": [ - "sha256:2238159eb743bd85304a16e0536048b3e991c531d1cd51c4a834d1ccf2829057", - "sha256:4df460394562b4581bb4e4087ad9447bd433148fba44241754ec3152499f1d1b" + "sha256:9d48dcccc213325e810fd723e7fbb45ccb39f6cf5c31f00cf2b965f5f10f3cb9", + "sha256:aa50258bbfa56d4e33fbd8aa3ef48ded10d1735f11532b8df95388cc6bdb7e83" ], - "markers": "python_version < '3.10'", - "version": "==5.0.7" + "markers": "python_version < '3.9'", + "version": "==6.1.0" }, "inflection": { "hashes": [ @@ -2324,11 +2332,11 @@ }, "ipython": { "hashes": [ - "sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1", - "sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf" + "sha256:c7b80eb7f5a855a88efc971fda506ff7a91c280b42cdae26643e0f601ea281ea", + "sha256:ea8801f15dfe4ffb76dea1b09b847430ffd70d827b41735c64a0638a04103bfc" ], "index": "pypi", - "version": "==8.14.0" + "version": "==8.12.2" }, "isort": { "hashes": [ @@ -2356,11 +2364,11 @@ }, "jsonschema": { "hashes": [ - "sha256:043dc26a3845ff09d20e4420d6012a9c91c9aa8999fa184e7efcfeccb41e32cb", - "sha256:6e1e7569ac13be8139b2dd2c21a55d350066ee3f80df06c608b398cdc6f30e8f" + "sha256:cd5f1f9ed9444e554b38ba003af06c0a8c2868131e56bfbef0550fb450c0330e", + "sha256:ec84cc37cfa703ef7cd4928db24f9cb31428a5d0fa77747b8b51a847458e0bbf" ], "markers": "python_version >= '3.8'", - "version": "==4.19.0" + "version": "==4.19.1" }, "jsonschema-specifications": { "hashes": [ @@ -2580,6 +2588,14 @@ ], "version": "==0.7.5" }, + "pkgutil-resolve-name": { + "hashes": [ + "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174", + "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e" + ], + "markers": "python_version < '3.9'", + "version": "==1.3.10" + }, "platformdirs": { "hashes": [ "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d", @@ -2729,6 +2745,13 @@ "index": "pypi", "version": "==2.8.2" }, + "pytz": { + "hashes": [ + "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b", + "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7" + ], + "version": "==2023.3.post1" + }, "pyyaml": { "hashes": [ "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", @@ -2803,121 +2826,121 @@ }, "resolvelib": { "hashes": [ - "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309", - "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf" + "sha256:c6ea56732e9fb6fca1b2acc2ccc68a0b6b8c566d8f3e78e0443310ede61dbd37", + "sha256:d9b7907f055c3b3a2cfc56c914ffd940122915826ff5fb5b1de0c99778f4de98" ], - "version": "==1.0.1" + "version": "==0.8.1" }, "rich": { "hashes": [ - "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808", - "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39" + "sha256:87b43e0543149efa1253f485cd845bb7ee54df16c9617b8a893650ab84b4acb6", + "sha256:9257b468badc3d347e146a4faa268ff229039d4c2d176ab0cffb4c4fbc73d5d9" ], "index": "pypi", - "version": "==13.5.2" + "version": "==13.5.3" }, "rpds-py": { "hashes": [ - "sha256:00215f6a9058fbf84f9d47536902558eb61f180a6b2a0fa35338d06ceb9a2e5a", - "sha256:0028eb0967942d0d2891eae700ae1a27b7fd18604cfcb16a1ef486a790fee99e", - "sha256:0155c33af0676fc38e1107679be882077680ad1abb6303956b97259c3177e85e", - "sha256:063411228b852fb2ed7485cf91f8e7d30893e69b0acb207ec349db04cccc8225", - "sha256:0700c2133ba203c4068aaecd6a59bda22e06a5e46255c9da23cbf68c6942215d", - "sha256:08e08ccf5b10badb7d0a5c84829b914c6e1e1f3a716fdb2bf294e2bd01562775", - "sha256:0d292cabd7c8335bdd3237ded442480a249dbcdb4ddfac5218799364a01a0f5c", - "sha256:15932ec5f224b0e35764dc156514533a4fca52dcfda0dfbe462a1a22b37efd59", - "sha256:18f87baa20e02e9277ad8960cd89b63c79c05caf106f4c959a9595c43f2a34a5", - "sha256:1a6420a36975e0073acaeee44ead260c1f6ea56812cfc6c31ec00c1c48197173", - "sha256:1b401e8b9aece651512e62c431181e6e83048a651698a727ea0eb0699e9f9b74", - "sha256:1d7b7b71bcb82d8713c7c2e9c5f061415598af5938666beded20d81fa23e7640", - "sha256:23750a9b8a329844ba1fe267ca456bb3184984da2880ed17ae641c5af8de3fef", - "sha256:23a059143c1393015c68936370cce11690f7294731904bdae47cc3e16d0b2474", - "sha256:26d9fd624649a10e4610fab2bc820e215a184d193e47d0be7fe53c1c8f67f370", - "sha256:291c9ce3929a75b45ce8ddde2aa7694fc8449f2bc8f5bd93adf021efaae2d10b", - "sha256:298e8b5d8087e0330aac211c85428c8761230ef46a1f2c516d6a2f67fb8803c5", - "sha256:2c7c4266c1b61eb429e8aeb7d8ed6a3bfe6c890a1788b18dbec090c35c6b93fa", - "sha256:2d68a8e8a3a816629283faf82358d8c93fe5bd974dd2704152394a3de4cec22a", - "sha256:344b89384c250ba6a4ce1786e04d01500e4dac0f4137ceebcaad12973c0ac0b3", - "sha256:3455ecc46ea443b5f7d9c2f946ce4017745e017b0d0f8b99c92564eff97e97f5", - "sha256:3d544a614055b131111bed6edfa1cb0fb082a7265761bcb03321f2dd7b5c6c48", - "sha256:3e5c26905aa651cc8c0ddc45e0e5dea2a1296f70bdc96af17aee9d0493280a17", - "sha256:3f5cc8c7bc99d2bbcd704cef165ca7d155cd6464c86cbda8339026a42d219397", - "sha256:4992266817169997854f81df7f6db7bdcda1609972d8ffd6919252f09ec3c0f6", - "sha256:4d55528ef13af4b4e074d067977b1f61408602f53ae4537dccf42ba665c2c7bd", - "sha256:576da63eae7809f375932bfcbca2cf20620a1915bf2fedce4b9cc8491eceefe3", - "sha256:58fc4d66ee349a23dbf08c7e964120dc9027059566e29cf0ce6205d590ed7eca", - "sha256:5b9bf77008f2c55dabbd099fd3ac87009471d223a1c7ebea36873d39511b780a", - "sha256:5e7996aed3f65667c6dcc8302a69368435a87c2364079a066750a2eac75ea01e", - "sha256:5f7487be65b9c2c510819e744e375bd41b929a97e5915c4852a82fbb085df62c", - "sha256:6388e4e95a26717b94a05ced084e19da4d92aca883f392dffcf8e48c8e221a24", - "sha256:65af12f70355de29e1092f319f85a3467f4005e959ab65129cb697169ce94b86", - "sha256:668d2b45d62c68c7a370ac3dce108ffda482b0a0f50abd8b4c604a813a59e08f", - "sha256:71333c22f7cf5f0480b59a0aef21f652cf9bbaa9679ad261b405b65a57511d1e", - "sha256:7150b83b3e3ddaac81a8bb6a9b5f93117674a0e7a2b5a5b32ab31fdfea6df27f", - "sha256:748e472345c3a82cfb462d0dff998a7bf43e621eed73374cb19f307e97e08a83", - "sha256:75dbfd41a61bc1fb0536bf7b1abf272dc115c53d4d77db770cd65d46d4520882", - "sha256:7618a082c55cf038eede4a918c1001cc8a4411dfe508dc762659bcd48d8f4c6e", - "sha256:780fcb855be29153901c67fc9c5633d48aebef21b90aa72812fa181d731c6b00", - "sha256:78d10c431073dc6ebceed35ab22948a016cc2b5120963c13a41e38bdde4a7212", - "sha256:7a3a3d3e4f1e3cd2a67b93a0b6ed0f2499e33f47cc568e3a0023e405abdc0ff1", - "sha256:7b6975d3763d0952c111700c0634968419268e6bbc0b55fe71138987fa66f309", - "sha256:80772e3bda6787510d9620bc0c7572be404a922f8ccdfd436bf6c3778119464c", - "sha256:80992eb20755701753e30a6952a96aa58f353d12a65ad3c9d48a8da5ec4690cf", - "sha256:841128a22e6ac04070a0f84776d07e9c38c4dcce8e28792a95e45fc621605517", - "sha256:861d25ae0985a1dd5297fee35f476b60c6029e2e6e19847d5b4d0a43a390b696", - "sha256:872f3dcaa8bf2245944861d7311179d2c0c9b2aaa7d3b464d99a7c2e401f01fa", - "sha256:87c93b25d538c433fb053da6228c6290117ba53ff6a537c133b0f2087948a582", - "sha256:8856aa76839dc234d3469f1e270918ce6bec1d6a601eba928f45d68a15f04fc3", - "sha256:885e023e73ce09b11b89ab91fc60f35d80878d2c19d6213a32b42ff36543c291", - "sha256:899b5e7e2d5a8bc92aa533c2d4e55e5ebba095c485568a5e4bedbc163421259a", - "sha256:8ce8caa29ebbdcde67e5fd652c811d34bc01f249dbc0d61e5cc4db05ae79a83b", - "sha256:8e1c68303ccf7fceb50fbab79064a2636119fd9aca121f28453709283dbca727", - "sha256:8e7e2b3577e97fa43c2c2b12a16139b2cedbd0770235d5179c0412b4794efd9b", - "sha256:92f05fc7d832e970047662b3440b190d24ea04f8d3c760e33e7163b67308c878", - "sha256:97f5811df21703446b42303475b8b855ee07d6ab6cdf8565eff115540624f25d", - "sha256:9affee8cb1ec453382c27eb9043378ab32f49cd4bc24a24275f5c39bf186c279", - "sha256:a2da4a8c6d465fde36cea7d54bf47b5cf089073452f0e47c8632ecb9dec23c07", - "sha256:a6903cdca64f1e301af9be424798328c1fe3b4b14aede35f04510989fc72f012", - "sha256:a8ab1adf04ae2d6d65835995218fd3f3eb644fe20655ca8ee233e2c7270ff53b", - "sha256:a8edd467551c1102dc0f5754ab55cd0703431cd3044edf8c8e7d9208d63fa453", - "sha256:ac00c41dd315d147b129976204839ca9de699d83519ff1272afbe4fb9d362d12", - "sha256:ad277f74b1c164f7248afa968700e410651eb858d7c160d109fb451dc45a2f09", - "sha256:ae46a50d235f1631d9ec4670503f7b30405103034830bc13df29fd947207f795", - "sha256:afe6b5a04b2ab1aa89bad32ca47bf71358e7302a06fdfdad857389dca8fb5f04", - "sha256:b1cb078f54af0abd835ca76f93a3152565b73be0f056264da45117d0adf5e99c", - "sha256:b25136212a3d064a8f0b9ebbb6c57094c5229e0de76d15c79b76feff26aeb7b8", - "sha256:b3226b246facae14909b465061ddcfa2dfeadb6a64f407f24300d42d69bcb1a1", - "sha256:b98e75b21fc2ba5285aef8efaf34131d16af1c38df36bdca2f50634bea2d3060", - "sha256:bbd7b24d108509a1b9b6679fcc1166a7dd031dbef1f3c2c73788f42e3ebb3beb", - "sha256:bed57543c99249ab3a4586ddc8786529fbc33309e5e8a1351802a06ca2baf4c2", - "sha256:c0583f69522732bdd79dca4cd3873e63a29acf4a299769c7541f2ca1e4dd4bc6", - "sha256:c1e0e9916301e3b3d970814b1439ca59487f0616d30f36a44cead66ee1748c31", - "sha256:c651847545422c8131660704c58606d841e228ed576c8f1666d98b3d318f89da", - "sha256:c7853f27195598e550fe089f78f0732c66ee1d1f0eaae8ad081589a5a2f5d4af", - "sha256:cbae50d352e4717ffc22c566afc2d0da744380e87ed44a144508e3fb9114a3f4", - "sha256:cdbed8f21204398f47de39b0a9b180d7e571f02dfb18bf5f1b618e238454b685", - "sha256:d08395595c42bcd82c3608762ce734504c6d025eef1c06f42326a6023a584186", - "sha256:d4639111e73997567343df6551da9dd90d66aece1b9fc26c786d328439488103", - "sha256:d63787f289944cc4bde518ad2b5e70a4f0d6e2ce76324635359c74c113fd188f", - "sha256:d6d5f061f6a2aa55790b9e64a23dfd87b6664ab56e24cd06c78eb43986cb260b", - "sha256:d7865df1fb564092bcf46dac61b5def25342faf6352e4bc0e61a286e3fa26a3d", - "sha256:db6585b600b2e76e98131e0ac0e5195759082b51687ad0c94505970c90718f4a", - "sha256:e36d7369363d2707d5f68950a64c4e025991eb0177db01ccb6aa6facae48b69f", - "sha256:e7947d9a6264c727a556541b1630296bbd5d0a05068d21c38dde8e7a1c703ef0", - "sha256:eb2d59bc196e6d3b1827c7db06c1a898bfa0787c0574af398e65ccf2e97c0fbe", - "sha256:ee9c2f6ca9774c2c24bbf7b23086264e6b5fa178201450535ec0859739e6f78d", - "sha256:f4760e1b02173f4155203054f77a5dc0b4078de7645c922b208d28e7eb99f3e2", - "sha256:f70bec8a14a692be6dbe7ce8aab303e88df891cbd4a39af091f90b6702e28055", - "sha256:f869e34d2326e417baee430ae998e91412cc8e7fdd83d979277a90a0e79a5b47", - "sha256:f8b9a7cd381970e64849070aca7c32d53ab7d96c66db6c2ef7aa23c6e803f514", - "sha256:f99d74ddf9d3b6126b509e81865f89bd1283e3fc1b568b68cd7bd9dfa15583d7", - "sha256:f9e7e493ded7042712a374471203dd43ae3fff5b81e3de1a0513fa241af9fd41", - "sha256:fc72ae476732cdb7b2c1acb5af23b478b8a0d4b6fcf19b90dd150291e0d5b26b", - "sha256:fccbf0cd3411719e4c9426755df90bf3449d9fc5a89f077f4a7f1abd4f70c910", - "sha256:ffcf18ad3edf1c170e27e88b10282a2c449aa0358659592462448d71b2000cfc" + "sha256:015de2ce2af1586ff5dc873e804434185199a15f7d96920ce67e50604592cae9", + "sha256:061c3ff1f51ecec256e916cf71cc01f9975af8fb3af9b94d3c0cc8702cfea637", + "sha256:08a80cf4884920863623a9ee9a285ee04cef57ebedc1cc87b3e3e0f24c8acfe5", + "sha256:09362f86ec201288d5687d1dc476b07bf39c08478cde837cb710b302864e7ec9", + "sha256:0bb4f48bd0dd18eebe826395e6a48b7331291078a879295bae4e5d053be50d4c", + "sha256:106af1653007cc569d5fbb5f08c6648a49fe4de74c2df814e234e282ebc06957", + "sha256:11fdd1192240dda8d6c5d18a06146e9045cb7e3ba7c06de6973000ff035df7c6", + "sha256:16a472300bc6c83fe4c2072cc22b3972f90d718d56f241adabc7ae509f53f154", + "sha256:176287bb998fd1e9846a9b666e240e58f8d3373e3bf87e7642f15af5405187b8", + "sha256:177914f81f66c86c012311f8c7f46887ec375cfcfd2a2f28233a3053ac93a569", + "sha256:177c9dd834cdf4dc39c27436ade6fdf9fe81484758885f2d616d5d03c0a83bd2", + "sha256:187700668c018a7e76e89424b7c1042f317c8df9161f00c0c903c82b0a8cac5c", + "sha256:1d9b5ee46dcb498fa3e46d4dfabcb531e1f2e76b477e0d99ef114f17bbd38453", + "sha256:22da15b902f9f8e267020d1c8bcfc4831ca646fecb60254f7bc71763569f56b1", + "sha256:24cd91a03543a0f8d09cb18d1cb27df80a84b5553d2bd94cba5979ef6af5c6e7", + "sha256:255f1a10ae39b52122cce26ce0781f7a616f502feecce9e616976f6a87992d6b", + "sha256:271c360fdc464fe6a75f13ea0c08ddf71a321f4c55fc20a3fe62ea3ef09df7d9", + "sha256:2ed83d53a8c5902ec48b90b2ac045e28e1698c0bea9441af9409fc844dc79496", + "sha256:2f3e1867dd574014253b4b8f01ba443b9c914e61d45f3674e452a915d6e929a3", + "sha256:35fbd23c1c8732cde7a94abe7fb071ec173c2f58c0bd0d7e5b669fdfc80a2c7b", + "sha256:37d0c59548ae56fae01c14998918d04ee0d5d3277363c10208eef8c4e2b68ed6", + "sha256:39d05e65f23a0fe897b6ac395f2a8d48c56ac0f583f5d663e0afec1da89b95da", + "sha256:3ad59efe24a4d54c2742929001f2d02803aafc15d6d781c21379e3f7f66ec842", + "sha256:3aed39db2f0ace76faa94f465d4234aac72e2f32b009f15da6492a561b3bbebd", + "sha256:3bbac1953c17252f9cc675bb19372444aadf0179b5df575ac4b56faaec9f6294", + "sha256:40bc802a696887b14c002edd43c18082cb7b6f9ee8b838239b03b56574d97f71", + "sha256:42f712b4668831c0cd85e0a5b5a308700fe068e37dcd24c0062904c4e372b093", + "sha256:448a66b8266de0b581246ca7cd6a73b8d98d15100fb7165974535fa3b577340e", + "sha256:485301ee56ce87a51ccb182a4b180d852c5cb2b3cb3a82f7d4714b4141119d8c", + "sha256:485747ee62da83366a44fbba963c5fe017860ad408ccd6cd99aa66ea80d32b2e", + "sha256:4cf0855a842c5b5c391dd32ca273b09e86abf8367572073bd1edfc52bc44446b", + "sha256:4eca20917a06d2fca7628ef3c8b94a8c358f6b43f1a621c9815243462dcccf97", + "sha256:4ed172d0c79f156c1b954e99c03bc2e3033c17efce8dd1a7c781bc4d5793dfac", + "sha256:5267cfda873ad62591b9332fd9472d2409f7cf02a34a9c9cb367e2c0255994bf", + "sha256:52b5cbc0469328e58180021138207e6ec91d7ca2e037d3549cc9e34e2187330a", + "sha256:53d7a3cd46cdc1689296348cb05ffd4f4280035770aee0c8ead3bbd4d6529acc", + "sha256:563646d74a4b4456d0cf3b714ca522e725243c603e8254ad85c3b59b7c0c4bf0", + "sha256:570cc326e78ff23dec7f41487aa9c3dffd02e5ee9ab43a8f6ccc3df8f9327623", + "sha256:5aca759ada6b1967fcfd4336dcf460d02a8a23e6abe06e90ea7881e5c22c4de6", + "sha256:5de11c041486681ce854c814844f4ce3282b6ea1656faae19208ebe09d31c5b8", + "sha256:5e271dd97c7bb8eefda5cca38cd0b0373a1fea50f71e8071376b46968582af9b", + "sha256:642ed0a209ced4be3a46f8cb094f2d76f1f479e2a1ceca6de6346a096cd3409d", + "sha256:6446002739ca29249f0beaaf067fcbc2b5aab4bc7ee8fb941bd194947ce19aff", + "sha256:691d50c99a937709ac4c4cd570d959a006bd6a6d970a484c84cc99543d4a5bbb", + "sha256:69b857a7d8bd4f5d6e0db4086da8c46309a26e8cefdfc778c0c5cc17d4b11e08", + "sha256:6ac3fefb0d168c7c6cab24fdfc80ec62cd2b4dfd9e65b84bdceb1cb01d385c33", + "sha256:6c9141af27a4e5819d74d67d227d5047a20fa3c7d4d9df43037a955b4c748ec5", + "sha256:7170cbde4070dc3c77dec82abf86f3b210633d4f89550fa0ad2d4b549a05572a", + "sha256:763ad59e105fca09705d9f9b29ecffb95ecdc3b0363be3bb56081b2c6de7977a", + "sha256:77076bdc8776a2b029e1e6ffbe6d7056e35f56f5e80d9dc0bad26ad4a024a762", + "sha256:7cd020b1fb41e3ab7716d4d2c3972d4588fdfbab9bfbbb64acc7078eccef8860", + "sha256:821392559d37759caa67d622d0d2994c7a3f2fb29274948ac799d496d92bca73", + "sha256:829e91f3a8574888b73e7a3feb3b1af698e717513597e23136ff4eba0bc8387a", + "sha256:850c272e0e0d1a5c5d73b1b7871b0a7c2446b304cec55ccdb3eaac0d792bb065", + "sha256:87d9b206b1bd7a0523375dc2020a6ce88bca5330682ae2fe25e86fd5d45cea9c", + "sha256:8bd01ff4032abaed03f2db702fa9a61078bee37add0bd884a6190b05e63b028c", + "sha256:8d54bbdf5d56e2c8cf81a1857250f3ea132de77af543d0ba5dce667183b61fec", + "sha256:8efaeb08ede95066da3a3e3c420fcc0a21693fcd0c4396d0585b019613d28515", + "sha256:8f94fdd756ba1f79f988855d948ae0bad9ddf44df296770d9a58c774cfbcca72", + "sha256:95cde244e7195b2c07ec9b73fa4c5026d4a27233451485caa1cd0c1b55f26dbd", + "sha256:975382d9aa90dc59253d6a83a5ca72e07f4ada3ae3d6c0575ced513db322b8ec", + "sha256:9dd9d9d9e898b9d30683bdd2b6c1849449158647d1049a125879cb397ee9cd12", + "sha256:a019a344312d0b1f429c00d49c3be62fa273d4a1094e1b224f403716b6d03be1", + "sha256:a4d9bfda3f84fc563868fe25ca160c8ff0e69bc4443c5647f960d59400ce6557", + "sha256:a657250807b6efd19b28f5922520ae002a54cb43c2401e6f3d0230c352564d25", + "sha256:a771417c9c06c56c9d53d11a5b084d1de75de82978e23c544270ab25e7c066ff", + "sha256:aad6ed9e70ddfb34d849b761fb243be58c735be6a9265b9060d6ddb77751e3e8", + "sha256:ae87137951bb3dc08c7d8bfb8988d8c119f3230731b08a71146e84aaa919a7a9", + "sha256:af247fd4f12cca4129c1b82090244ea5a9d5bb089e9a82feb5a2f7c6a9fe181d", + "sha256:b5d4bdd697195f3876d134101c40c7d06d46c6ab25159ed5cbd44105c715278a", + "sha256:b9255e7165083de7c1d605e818025e8860636348f34a79d84ec533546064f07e", + "sha256:c22211c165166de6683de8136229721f3d5c8606cc2c3d1562da9a3a5058049c", + "sha256:c55f9821f88e8bee4b7a72c82cfb5ecd22b6aad04033334f33c329b29bfa4da0", + "sha256:c7aed97f2e676561416c927b063802c8a6285e9b55e1b83213dfd99a8f4f9e48", + "sha256:cd2163f42868865597d89399a01aa33b7594ce8e2c4a28503127c81a2f17784e", + "sha256:ce5e7504db95b76fc89055c7f41e367eaadef5b1d059e27e1d6eabf2b55ca314", + "sha256:cff7351c251c7546407827b6a37bcef6416304fc54d12d44dbfecbb717064717", + "sha256:d27aa6bbc1f33be920bb7adbb95581452cdf23005d5611b29a12bb6a3468cc95", + "sha256:d3b52a67ac66a3a64a7e710ba629f62d1e26ca0504c29ee8cbd99b97df7079a8", + "sha256:de61e424062173b4f70eec07e12469edde7e17fa180019a2a0d75c13a5c5dc57", + "sha256:e10e6a1ed2b8661201e79dff5531f8ad4cdd83548a0f81c95cf79b3184b20c33", + "sha256:e1a0ffc39f51aa5f5c22114a8f1906b3c17eba68c5babb86c5f77d8b1bba14d1", + "sha256:e22491d25f97199fc3581ad8dd8ce198d8c8fdb8dae80dea3512e1ce6d5fa99f", + "sha256:e626b864725680cd3904414d72e7b0bd81c0e5b2b53a5b30b4273034253bb41f", + "sha256:e8c71ea77536149e36c4c784f6d420ffd20bea041e3ba21ed021cb40ce58e2c9", + "sha256:e8d0f0eca087630d58b8c662085529781fd5dc80f0a54eda42d5c9029f812599", + "sha256:ea65b59882d5fa8c74a23f8960db579e5e341534934f43f3b18ec1839b893e41", + "sha256:ea93163472db26ac6043e8f7f93a05d9b59e0505c760da2a3cd22c7dd7111391", + "sha256:eab75a8569a095f2ad470b342f2751d9902f7944704f0571c8af46bede438475", + "sha256:ed8313809571a5463fd7db43aaca68ecb43ca7a58f5b23b6e6c6c5d02bdc7882", + "sha256:ef5fddfb264e89c435be4adb3953cef5d2936fdeb4463b4161a6ba2f22e7b740", + "sha256:ef750a20de1b65657a1425f77c525b0183eac63fe7b8f5ac0dd16f3668d3e64f", + "sha256:efb9ece97e696bb56e31166a9dd7919f8f0c6b31967b454718c6509f29ef6fee", + "sha256:f4c179a7aeae10ddf44c6bac87938134c1379c49c884529f090f9bf05566c836", + "sha256:f602881d80ee4228a2355c68da6b296a296cd22bbb91e5418d54577bbf17fa7c", + "sha256:fc2200e79d75b5238c8d69f6a30f8284290c777039d331e7340b6c17cad24a5a", + "sha256:fcc1ebb7561a3e24a6588f7c6ded15d80aec22c66a070c757559b57b17ffd1cb" ], "markers": "python_version >= '3.8'", - "version": "==0.10.0" + "version": "==0.10.3" }, "ruamel.yaml": { "hashes": [ From 45860e8822904538574d36a75f4e1c961cdc5620 Mon Sep 17 00:00:00 2001 From: Oliwia Zaremba Date: Thu, 21 Sep 2023 14:30:58 +0200 Subject: [PATCH 30/30] Add merge migration --- apps/accounts/migrations/0062_merge_20230921_1230.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 apps/accounts/migrations/0062_merge_20230921_1230.py diff --git a/apps/accounts/migrations/0062_merge_20230921_1230.py b/apps/accounts/migrations/0062_merge_20230921_1230.py new file mode 100644 index 00000000..370c0e17 --- /dev/null +++ b/apps/accounts/migrations/0062_merge_20230921_1230.py @@ -0,0 +1,12 @@ +# Generated by Django 3.2.21 on 2023-09-21 12:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("accounts", "0060_alter_hostingprovider_website"), + ("accounts", "0061_providerrequest_rename_open_status"), + ] + + operations = []