From 0b4cdf7a69b5fe10c82c696a08ca5dd8ef10db34 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 17 Jun 2024 17:50:10 -0400 Subject: [PATCH 01/28] Fix delete request bug to ensure event listeners are not duplicated --- src/registrar/assets/js/get-gov.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 86bb1fe6e..371271c8b 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1507,8 +1507,13 @@ document.addEventListener('DOMContentLoaded', function() { modals.forEach(modal => { const submitButton = modal.querySelector('.usa-modal__submit'); const closeButton = modal.querySelector('.usa-modal__close'); - submitButton.addEventListener('click', function() { - pk = submitButton.getAttribute('data-pk'); + + // Clone the submit button to remove all existing event listeners + const newSubmitButton = submitButton.cloneNode(true); + submitButton.parentNode.replaceChild(newSubmitButton, submitButton); + + newSubmitButton.addEventListener('click', function() { + pk = newSubmitButton.getAttribute('data-pk'); // Close the modal to remove the USWDS UI local classes closeButton.click(); // If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page From cbf413cdddd83ddb173c2c613a86fa68f9a12946 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 18 Jun 2024 12:11:50 -0600 Subject: [PATCH 02/28] fixed asterisks --- .../templates/domain_request_additional_details.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/domain_request_additional_details.html b/src/registrar/templates/domain_request_additional_details.html index c78bc8e96..2a581bbd2 100644 --- a/src/registrar/templates/domain_request_additional_details.html +++ b/src/registrar/templates/domain_request_additional_details.html @@ -16,7 +16,7 @@ - Select one (*). + Select one. * {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} {% input_with_errors forms.0.has_cisa_representative %} {% endwith %} @@ -36,7 +36,7 @@ - Select one (*). + Select one. * {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} {% input_with_errors forms.2.has_anything_else_text %} {% endwith %} @@ -44,7 +44,7 @@
-

Provide details below (*).

+

Provide details below. *

{% with attr_maxlength=2000 add_label_class="usa-sr-only" %} {% input_with_errors forms.3.anything_else %} {% endwith %} From ff3d18eeaefc97a4dadd7e5d636b6775f3c6bfeb Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 18 Jun 2024 14:57:48 -0400 Subject: [PATCH 03/28] trace down the modal unload error and fix it in uswds --- src/registrar/assets/js/get-gov.js | 65 ++++++++++++------------- src/registrar/assets/js/uswds-edited.js | 19 +++++--- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 371271c8b..4c4f9a9c6 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -33,6 +33,33 @@ const showElement = (element) => { element.classList.remove('display-none'); }; +/** + * Helper function that scrolls to an element + * @param {string} attributeName - The string "class" or "id" + * @param {string} attributeValue - The class or id name + */ +function ScrollToElement(attributeName, attributeValue) { + let targetEl = null; + + if (attributeName === 'class') { + targetEl = document.getElementsByClassName(attributeValue)[0]; + } else if (attributeName === 'id') { + targetEl = document.getElementById(attributeValue); + } else { + console.log('Error: unknown attribute name provided.'); + return; // Exit the function if an invalid attributeName is provided + } + + if (targetEl) { + const rect = targetEl.getBoundingClientRect(); + const scrollTop = window.scrollY || document.documentElement.scrollTop; + window.scrollTo({ + top: rect.top + scrollTop, + behavior: 'smooth' // Optional: for smooth scrolling + }); + } +} + /** Makes an element invisible. */ function makeHidden(el) { el.style.position = "absolute"; @@ -895,33 +922,6 @@ function unloadModals() { window.modal.off(); } -/** - * Helper function that scrolls to an element - * @param {string} attributeName - The string "class" or "id" - * @param {string} attributeValue - The class or id name - */ -function ScrollToElement(attributeName, attributeValue) { - let targetEl = null; - - if (attributeName === 'class') { - targetEl = document.getElementsByClassName(attributeValue)[0]; - } else if (attributeName === 'id') { - targetEl = document.getElementById(attributeValue); - } else { - console.log('Error: unknown attribute name provided.'); - return; // Exit the function if an invalid attributeName is provided - } - - if (targetEl) { - const rect = targetEl.getBoundingClientRect(); - const scrollTop = window.scrollY || document.documentElement.scrollTop; - window.scrollTo({ - top: rect.top + scrollTop, - behavior: 'smooth' // Optional: for smooth scrolling - }); - } -} - /** * Generalized function to update pagination for a list. * @param {string} itemName - The name displayed in the counter @@ -1469,7 +1469,7 @@ document.addEventListener('DOMContentLoaded', function() {
- ` + ` domainRequestsSectionWrapper.appendChild(modal); } @@ -1507,13 +1507,10 @@ document.addEventListener('DOMContentLoaded', function() { modals.forEach(modal => { const submitButton = modal.querySelector('.usa-modal__submit'); const closeButton = modal.querySelector('.usa-modal__close'); - - // Clone the submit button to remove all existing event listeners - const newSubmitButton = submitButton.cloneNode(true); - submitButton.parentNode.replaceChild(newSubmitButton, submitButton); - newSubmitButton.addEventListener('click', function() { - pk = newSubmitButton.getAttribute('data-pk'); + + submitButton.addEventListener('click', function() { + pk = submitButton.getAttribute('data-pk'); // Close the modal to remove the USWDS UI local classes closeButton.click(); // If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page diff --git a/src/registrar/assets/js/uswds-edited.js b/src/registrar/assets/js/uswds-edited.js index e73f3b6c0..f849e944e 100644 --- a/src/registrar/assets/js/uswds-edited.js +++ b/src/registrar/assets/js/uswds-edited.js @@ -5311,14 +5311,17 @@ const cleanUpModal = baseComponent => { const modalID = modalWrapper.getAttribute("id"); const originalLocationPlaceHolder = document.querySelector(`[data-placeholder-for="${modalID}"]`); if (originalLocationPlaceHolder) { - for (let attributeIndex = 0; attributeIndex < originalLocationPlaceHolder.attributes.length; attributeIndex += 1) { - const attribute = originalLocationPlaceHolder.attributes[attributeIndex]; - if (attribute.name.startsWith('data-original-')) { - // data-original- is 14 long - modalContent.setAttribute(attribute.name.substr(14), attribute.value); - } - } - originalLocationPlaceHolder.after(modalContent); + // DOTGOV + // Why is this line here? It seems to be recreating the original placeholder and then adding a + // copy of it after that first placeholder, which is netralizing our call of off() + // for (let attributeIndex = 0; attributeIndex < originalLocationPlaceHolder.attributes.length; attributeIndex += 1) { + // const attribute = originalLocationPlaceHolder.attributes[attributeIndex]; + // if (attribute.name.startsWith('data-original-')) { + // // data-original- is 14 long + // modalContent.setAttribute(attribute.name.substr(14), attribute.value); + // } + // } + //originalLocationPlaceHolder.after(modalContent); originalLocationPlaceHolder.parentElement.removeChild(originalLocationPlaceHolder); } modalWrapper.parentElement.removeChild(modalWrapper); From f608e0b99c3f1279abece406cb6517426902ccab Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 18 Jun 2024 16:56:08 -0400 Subject: [PATCH 04/28] revise solution to override uswds code instead of deleting it --- src/registrar/assets/js/get-gov.js | 13 +++++++++++++ src/registrar/assets/js/uswds-edited.js | 19 ++++++++----------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 4c4f9a9c6..4388395d4 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1294,6 +1294,10 @@ document.addEventListener('DOMContentLoaded', function() { * @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page */ function deleteDomainRequest(domainRequestPk,pageToDisplay) { + + // Use to debug uswds modal issues + //console.log('deleteDomainRequest') + // Get csrf token const csrfToken = getCsrfToken(); // Create FormData object and append the CSRF token @@ -1348,6 +1352,15 @@ document.addEventListener('DOMContentLoaded', function() { const tbody = document.querySelector('.domain-requests__table tbody'); tbody.innerHTML = ''; + // Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases + // We do NOT want that as it will cause multiple placeholders and therfore multiple inits on delete, + // which will cause bad delete requests to be sent. + const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]'); + preExistingModalPlaceholders.forEach(element => { + console.log('found one'); + element.remove(); + }); + // remove any existing modal elements from the DOM so they can be properly re-initialized // after the DOM content changes and there are new delete modal buttons added unloadModals(); diff --git a/src/registrar/assets/js/uswds-edited.js b/src/registrar/assets/js/uswds-edited.js index f849e944e..e73f3b6c0 100644 --- a/src/registrar/assets/js/uswds-edited.js +++ b/src/registrar/assets/js/uswds-edited.js @@ -5311,17 +5311,14 @@ const cleanUpModal = baseComponent => { const modalID = modalWrapper.getAttribute("id"); const originalLocationPlaceHolder = document.querySelector(`[data-placeholder-for="${modalID}"]`); if (originalLocationPlaceHolder) { - // DOTGOV - // Why is this line here? It seems to be recreating the original placeholder and then adding a - // copy of it after that first placeholder, which is netralizing our call of off() - // for (let attributeIndex = 0; attributeIndex < originalLocationPlaceHolder.attributes.length; attributeIndex += 1) { - // const attribute = originalLocationPlaceHolder.attributes[attributeIndex]; - // if (attribute.name.startsWith('data-original-')) { - // // data-original- is 14 long - // modalContent.setAttribute(attribute.name.substr(14), attribute.value); - // } - // } - //originalLocationPlaceHolder.after(modalContent); + for (let attributeIndex = 0; attributeIndex < originalLocationPlaceHolder.attributes.length; attributeIndex += 1) { + const attribute = originalLocationPlaceHolder.attributes[attributeIndex]; + if (attribute.name.startsWith('data-original-')) { + // data-original- is 14 long + modalContent.setAttribute(attribute.name.substr(14), attribute.value); + } + } + originalLocationPlaceHolder.after(modalContent); originalLocationPlaceHolder.parentElement.removeChild(originalLocationPlaceHolder); } modalWrapper.parentElement.removeChild(modalWrapper); From 0d3e38ce70c031ec87be12064087df9fc0b9dc35 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 18 Jun 2024 16:57:27 -0400 Subject: [PATCH 05/28] cleanup --- src/registrar/assets/js/get-gov.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 4388395d4..aa0230043 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1520,8 +1520,6 @@ document.addEventListener('DOMContentLoaded', function() { modals.forEach(modal => { const submitButton = modal.querySelector('.usa-modal__submit'); const closeButton = modal.querySelector('.usa-modal__close'); - - submitButton.addEventListener('click', function() { pk = submitButton.getAttribute('data-pk'); // Close the modal to remove the USWDS UI local classes From 4b1ca63016a0f6bc156122f7a03720c7878f5dc4 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 18 Jun 2024 17:01:06 -0400 Subject: [PATCH 06/28] cleanup --- src/registrar/assets/js/get-gov.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index aa0230043..45f37a082 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1353,7 +1353,7 @@ document.addEventListener('DOMContentLoaded', function() { tbody.innerHTML = ''; // Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases - // We do NOT want that as it will cause multiple placeholders and therfore multiple inits on delete, + // We do NOT want that as it will cause multiple placeholders and therefore multiple inits on delete, // which will cause bad delete requests to be sent. const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]'); preExistingModalPlaceholders.forEach(element => { From 97139076aeeb1ba333a0bd73c40e478df7f5026f Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 18 Jun 2024 17:56:25 -0600 Subject: [PATCH 07/28] Added portfolio to admin frontend for domain_request and domain_information --- src/registrar/admin.py | 3 ++- src/registrar/models/portfolio.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8a691c7fa..ed43661dc 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1233,7 +1233,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): search_help_text = "Search by domain." fieldsets = [ - (None, {"fields": ["creator", "submitter", "domain_request", "notes"]}), + (None, {"fields": ["portfolio","creator", "submitter", "domain_request", "notes"]}), (".gov domain", {"fields": ["domain"]}), ("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}), ("Background info", {"fields": ["anything_else"]}), @@ -1482,6 +1482,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): None, { "fields": [ + "portfolio", "status", "rejection_reason", "action_needed_reason", diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index a05422960..9fefacc31 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -97,3 +97,6 @@ class Portfolio(TimeStampedModel): verbose_name="security contact e-mail", max_length=320, ) + + def __str__(self) -> str: + return f"{self.organization_name}" \ No newline at end of file From df719f2108971caedd1f9af59e285814b22ff67f Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 19 Jun 2024 22:55:20 -0600 Subject: [PATCH 08/28] Added logic to hide certain fields from non-superusers in Admin --- src/registrar/admin.py | 81 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ed43661dc..51fad1c58 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1233,7 +1233,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): search_help_text = "Search by domain." fieldsets = [ - (None, {"fields": ["portfolio","creator", "submitter", "domain_request", "notes"]}), + (None, {"fields": ["portfolio", "creator", "submitter", "domain_request", "notes"]}), (".gov domain", {"fields": ["domain"]}), ("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}), ("Background info", {"fields": ["anything_else"]}), @@ -1319,6 +1319,33 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): change_form_template = "django/admin/domain_information_change_form.html" + + superuser_only_fields = [ + "portfolio", + ] + + # DEVELOPER's NOTE: + # Normally, to exclude a field from an Admin form, we could simply utilize + # Django's "exclude" feature. However, it causes a "missing key" error if we + # go that route for this particular form. The error gets thrown by our + # custom fieldset.html code and is due to the fact that "exclude" removes + # fields from base_fields but not fieldsets. Rather than reworking our + # custom frontend, it seems more straightforward (and easier to read) to simply + # modify the fieldsets list so that it excludes any fields we want to remove + # based on permissions (eg. superuser_only_fields) or other conditions. + def get_fieldsets(self, request, obj=None): + fieldsets = super().get_fieldsets(request, obj) + + # Create a modified version of fieldsets without the 'isbn' field + if not request.user.has_perm("registrar.full_access_permission"): + modified_fieldsets = [] + for name, data in fieldsets: + fields = data.get('fields', []) + fields = tuple(field for field in fields if field not in self.superuser_only_fields) + modified_fieldsets.append((name, {'fields': fields})) + return modified_fieldsets + return fieldsets + def get_readonly_fields(self, request, obj=None): """Set the read-only state on form elements. We have 1 conditions that determine which fields are read-only: @@ -1593,6 +1620,53 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): ] filter_horizontal = ("current_websites", "alternative_domains", "other_contacts") + superuser_only_fields = [ + "portfolio", + ] + + # DEVELOPER's NOTE: + # Normally, to exclude a field from an Admin form, we could simply utilize + # Django's "exclude" feature. However, it causes a "missing key" error if we + # go that route for this particular form. The error gets thrown by our + # custom fieldset.html code and is due to the fact that "exclude" removes + # fields from base_fields but not fieldsets. Rather than reworking our + # custom frontend, it seems more straightforward (and easier to read) to simply + # modify the fieldsets list so that it excludes any fields we want to remove + # based on permissions (eg. superuser_only_fields) or other conditions. + def get_fieldsets(self, request, obj=None): + fieldsets = super().get_fieldsets(request, obj) + + # Create a modified version of fieldsets without the 'isbn' field + if not request.user.has_perm("registrar.full_access_permission"): + modified_fieldsets = [] + for name, data in fieldsets: + fields = data.get('fields', []) + fields = tuple(field for field in fields if field not in self.superuser_only_fields) + modified_fieldsets.append((name, {'fields': fields})) + return modified_fieldsets + return fieldsets + + # Fields only superusers can view + # exclude = ['address_line1', ] + # widgets = {'portfolio': forms.HiddenInput()} + + + # def get_form(self, request, obj, **kwargs): + # if request.user.has_perm("registrar.full_access_permission"): + # self.exclude = self.superuser_only_fields + # # self.fieldsets[1][1]['fields'][0].append('portfolio') + # # self.fieldsets[1][1]['fields'].pop('status') + # form = super(DomainRequestAdmin, self).get_form(request, obj, **kwargs) + # return form + + + # if not request.user.has_perm("registrar.full_access_permission"): + + # for fieldset in self.fieldsets: + # for field in fieldset[0]["fields"]: + # if field== + + # Table ordering # NOTE: This impacts the select2 dropdowns (combobox) # Currentl, there's only one for requests on DomainInfo @@ -1851,6 +1925,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): return super().change_view(request, object_id, form_url, extra_context) def process_log_entry(self, log_entry): + """Process a log entry and return filtered entry dictionary if applicable.""" changes = log_entry.changes status_changed = "status" in changes @@ -1906,7 +1981,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): return entry return None - + + class TransitionDomainAdmin(ListHeaderAdmin): """Custom transition domain admin class.""" @@ -2562,6 +2638,7 @@ class PortfolioAdmin(ListHeaderAdmin): # "requestor", # ] + def save_model(self, request, obj, form, change): if obj.creator is not None: From 7c2c9a1f273c7639f6ed8a1ac56c84f04d96ebb2 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 19 Jun 2024 23:00:01 -0600 Subject: [PATCH 09/28] linted --- src/registrar/admin.py | 49 ++++++++----------------------- src/registrar/models/portfolio.py | 2 +- 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 51fad1c58..65f287adc 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1319,33 +1319,32 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): change_form_template = "django/admin/domain_information_change_form.html" - superuser_only_fields = [ "portfolio", ] # DEVELOPER's NOTE: - # Normally, to exclude a field from an Admin form, we could simply utilize + # Normally, to exclude a field from an Admin form, we could simply utilize # Django's "exclude" feature. However, it causes a "missing key" error if we # go that route for this particular form. The error gets thrown by our # custom fieldset.html code and is due to the fact that "exclude" removes - # fields from base_fields but not fieldsets. Rather than reworking our + # fields from base_fields but not fieldsets. Rather than reworking our # custom frontend, it seems more straightforward (and easier to read) to simply # modify the fieldsets list so that it excludes any fields we want to remove # based on permissions (eg. superuser_only_fields) or other conditions. def get_fieldsets(self, request, obj=None): fieldsets = super().get_fieldsets(request, obj) - + # Create a modified version of fieldsets without the 'isbn' field if not request.user.has_perm("registrar.full_access_permission"): modified_fieldsets = [] for name, data in fieldsets: - fields = data.get('fields', []) + fields = data.get("fields", []) fields = tuple(field for field in fields if field not in self.superuser_only_fields) - modified_fieldsets.append((name, {'fields': fields})) + modified_fieldsets.append((name, {"fields": fields})) return modified_fieldsets return fieldsets - + def get_readonly_fields(self, request, obj=None): """Set the read-only state on form elements. We have 1 conditions that determine which fields are read-only: @@ -1625,48 +1624,27 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): ] # DEVELOPER's NOTE: - # Normally, to exclude a field from an Admin form, we could simply utilize + # Normally, to exclude a field from an Admin form, we could simply utilize # Django's "exclude" feature. However, it causes a "missing key" error if we # go that route for this particular form. The error gets thrown by our # custom fieldset.html code and is due to the fact that "exclude" removes - # fields from base_fields but not fieldsets. Rather than reworking our + # fields from base_fields but not fieldsets. Rather than reworking our # custom frontend, it seems more straightforward (and easier to read) to simply # modify the fieldsets list so that it excludes any fields we want to remove # based on permissions (eg. superuser_only_fields) or other conditions. def get_fieldsets(self, request, obj=None): fieldsets = super().get_fieldsets(request, obj) - + # Create a modified version of fieldsets without the 'isbn' field if not request.user.has_perm("registrar.full_access_permission"): modified_fieldsets = [] for name, data in fieldsets: - fields = data.get('fields', []) + fields = data.get("fields", []) fields = tuple(field for field in fields if field not in self.superuser_only_fields) - modified_fieldsets.append((name, {'fields': fields})) + modified_fieldsets.append((name, {"fields": fields})) return modified_fieldsets return fieldsets - # Fields only superusers can view - # exclude = ['address_line1', ] - # widgets = {'portfolio': forms.HiddenInput()} - - - # def get_form(self, request, obj, **kwargs): - # if request.user.has_perm("registrar.full_access_permission"): - # self.exclude = self.superuser_only_fields - # # self.fieldsets[1][1]['fields'][0].append('portfolio') - # # self.fieldsets[1][1]['fields'].pop('status') - # form = super(DomainRequestAdmin, self).get_form(request, obj, **kwargs) - # return form - - - # if not request.user.has_perm("registrar.full_access_permission"): - - # for fieldset in self.fieldsets: - # for field in fieldset[0]["fields"]: - # if field== - - # Table ordering # NOTE: This impacts the select2 dropdowns (combobox) # Currentl, there's only one for requests on DomainInfo @@ -1925,7 +1903,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): return super().change_view(request, object_id, form_url, extra_context) def process_log_entry(self, log_entry): - """Process a log entry and return filtered entry dictionary if applicable.""" changes = log_entry.changes status_changed = "status" in changes @@ -1981,8 +1958,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): return entry return None - - + class TransitionDomainAdmin(ListHeaderAdmin): """Custom transition domain admin class.""" @@ -2638,7 +2614,6 @@ class PortfolioAdmin(ListHeaderAdmin): # "requestor", # ] - def save_model(self, request, obj, form, change): if obj.creator is not None: diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 9fefacc31..0ea036bb7 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -99,4 +99,4 @@ class Portfolio(TimeStampedModel): ) def __str__(self) -> str: - return f"{self.organization_name}" \ No newline at end of file + return f"{self.organization_name}" From 7a5de9c3ddbdae889f915cb86a06bd56a88f29c2 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 20 Jun 2024 13:49:32 -0600 Subject: [PATCH 10/28] (updated comments) --- src/registrar/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 65f287adc..fa132724a 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1335,7 +1335,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): def get_fieldsets(self, request, obj=None): fieldsets = super().get_fieldsets(request, obj) - # Create a modified version of fieldsets without the 'isbn' field + # Create a modified version of fieldsets to exclude certain fields if not request.user.has_perm("registrar.full_access_permission"): modified_fieldsets = [] for name, data in fieldsets: @@ -1635,7 +1635,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): def get_fieldsets(self, request, obj=None): fieldsets = super().get_fieldsets(request, obj) - # Create a modified version of fieldsets without the 'isbn' field + # Create a modified version of fieldsets to exclude certain fields if not request.user.has_perm("registrar.full_access_permission"): modified_fieldsets = [] for name, data in fieldsets: From 343fe4e2dbcdfbd9ab272f314746df2d78e2c555 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 20 Jun 2024 14:34:20 -0600 Subject: [PATCH 11/28] Fixed logic for DomainInformationInline form --- src/registrar/admin.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index fa132724a..8d53545cf 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1333,14 +1333,14 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): # modify the fieldsets list so that it excludes any fields we want to remove # based on permissions (eg. superuser_only_fields) or other conditions. def get_fieldsets(self, request, obj=None): - fieldsets = super().get_fieldsets(request, obj) + fieldsets = self.fieldsets # Create a modified version of fieldsets to exclude certain fields if not request.user.has_perm("registrar.full_access_permission"): modified_fieldsets = [] for name, data in fieldsets: fields = data.get("fields", []) - fields = tuple(field for field in fields if field not in self.superuser_only_fields) + fields = tuple(field for field in fields if field not in DomainInformationAdmin.superuser_only_fields) modified_fieldsets.append((name, {"fields": fields})) return modified_fieldsets return fieldsets @@ -1990,13 +1990,7 @@ class DomainInformationInline(admin.StackedInline): template = "django/admin/includes/domain_info_inline_stacked.html" model = models.DomainInformation - fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets) - # remove .gov domain from fieldset - for index, (title, f) in enumerate(fieldsets): - if title == ".gov domain": - del fieldsets[index] - break - + fieldsets = DomainInformationAdmin.fieldsets readonly_fields = DomainInformationAdmin.readonly_fields analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields @@ -2042,6 +2036,23 @@ class DomainInformationInline(admin.StackedInline): def get_readonly_fields(self, request, obj=None): return DomainInformationAdmin.get_readonly_fields(self, request, obj=None) + + # Re-route the get_fieldsets method to utilize DomainInformationAdmin.get_fieldsets + # since that has all the logic for excluding certain fields according to user permissions. + # Then modify the remaining fields to further trim out any we don't want for this inline + # form + def get_fieldsets(self, request, obj=None): + # Grab fieldsets from DomainInformationAdmin so that it handles all logic + # for permission-based field visibility. + modified_fieldsets = DomainInformationAdmin.get_fieldsets(self, request, obj=None) + + # remove .gov domain from fieldset + for index, (title, f) in enumerate(modified_fieldsets): + if title == ".gov domain": + del modified_fieldsets[index] + break + + return modified_fieldsets class DomainResource(FsmModelResource): From 7072125992207d62e95c59a24a43097acb55122c Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 20 Jun 2024 16:40:20 -0400 Subject: [PATCH 12/28] renamed organization to portfolio, removed obsolete code --- src/registrar/config/settings.py | 2 +- src/registrar/config/urls.py | 14 +++--- src/registrar/registrar_middleware.py | 10 ++--- src/registrar/templates/organization.html | 43 ------------------- src/registrar/templates/portfolio.html | 24 +++++++++++ ...on_domains.html => portfolio_domains.html} | 4 +- ..._requests.html => portfolio_requests.html} | 4 +- ...on_sidebar.html => portfolio_sidebar.html} | 4 +- .../views/{organizations.py => portfolios.py} | 8 ++-- 9 files changed, 47 insertions(+), 66 deletions(-) delete mode 100644 src/registrar/templates/organization.html create mode 100644 src/registrar/templates/portfolio.html rename src/registrar/templates/{organization_domains.html => portfolio_domains.html} (65%) rename src/registrar/templates/{organization_requests.html => portfolio_requests.html} (88%) rename src/registrar/templates/{organization_sidebar.html => portfolio_sidebar.html} (87%) rename src/registrar/views/{organizations.py => portfolios.py} (85%) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index aa7d73c2f..688a3e8ca 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -189,7 +189,7 @@ MIDDLEWARE = [ # Used for waffle feature flags "waffle.middleware.WaffleMiddleware", "registrar.registrar_middleware.CheckUserProfileMiddleware", - "registrar.registrar_middleware.CheckOrganizationMiddleware", + "registrar.registrar_middleware.CheckPortfolioMiddleware", ] # application object used by Django’s built-in servers (e.g. `runserver`) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 3e0d44c4c..b81fba023 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -25,7 +25,7 @@ from registrar.views.domain_request import Step from registrar.views.domain_requests_json import get_domain_requests_json from registrar.views.domains_json import get_domains_json from registrar.views.utility import always_404 -from registrar.views.organizations import organization_domains, organization_domain_requests +from registrar.views.portfolios import portfolio_domains, portfolio_domain_requests from api.views import available, get_current_federal, get_current_full @@ -60,14 +60,14 @@ for step, view in [ urlpatterns = [ path("", views.index, name="home"), path( - "organization//domains/", - organization_domains, - name="organization-domains", + "portfolio//domains/", + portfolio_domains, + name="portfolio-domains", ), path( - "organization//domain_requests/", - organization_domain_requests, - name="organization-domain-requests", + "portfolio//domain_requests/", + portfolio_domain_requests, + name="portfolio-domain-requests", ), path( "admin/logout/", diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index ebb0e291d..880f9ee40 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -125,10 +125,10 @@ class CheckUserProfileMiddleware: return None -class CheckOrganizationMiddleware: +class CheckPortfolioMiddleware: """ Checks if the current user has a portfolio - If they do, redirect them to the org homepage when they navigate to home. + If they do, redirect them to the portfolio homepage when they navigate to home. """ def __init__(self, get_response): @@ -150,8 +150,8 @@ class CheckOrganizationMiddleware: user_portfolios = Portfolio.objects.filter(creator=request.user) if user_portfolios.exists(): first_portfolio = user_portfolios.first() - home_organization_with_portfolio = reverse( - "organization-domains", kwargs={"portfolio_id": first_portfolio.id} + home_with_portfolio = reverse( + "portfolio-domains", kwargs={"portfolio_id": first_portfolio.id} ) - return HttpResponseRedirect(home_organization_with_portfolio) + return HttpResponseRedirect(home_with_portfolio) return None diff --git a/src/registrar/templates/organization.html b/src/registrar/templates/organization.html deleted file mode 100644 index 33ab7ff6c..000000000 --- a/src/registrar/templates/organization.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends 'home.html' %} - -{% load static %} - -{% block homepage_content %} - -
-
-
- {% include "organization_sidebar.html" with portfolio=portfolio %} -
-
- {% block messages %} - {% include "includes/form_messages.html" %} - {% endblock %} - {# Note: Reimplement commented out functionality #} - - {% block organization_content %} - {% endblock %} - - {# Note: Reimplement this after MVP #} - - - - - - -
-
- -{% endblock %} diff --git a/src/registrar/templates/portfolio.html b/src/registrar/templates/portfolio.html new file mode 100644 index 000000000..4f37c0175 --- /dev/null +++ b/src/registrar/templates/portfolio.html @@ -0,0 +1,24 @@ +{% extends 'home.html' %} + +{% load static %} + +{% block homepage_content %} + +
+
+
+ {% include "portfolio_sidebar.html" with portfolio=portfolio %} +
+
+ {% block messages %} + {% include "includes/form_messages.html" %} + {% endblock %} + {# Note: Reimplement commented out functionality #} + + {% block portfolio_content %} + {% endblock %} + +
+
+ +{% endblock %} diff --git a/src/registrar/templates/organization_domains.html b/src/registrar/templates/portfolio_domains.html similarity index 65% rename from src/registrar/templates/organization_domains.html rename to src/registrar/templates/portfolio_domains.html index 9dff1fb0b..3009b4b14 100644 --- a/src/registrar/templates/organization_domains.html +++ b/src/registrar/templates/portfolio_domains.html @@ -1,8 +1,8 @@ -{% extends 'organization.html' %} +{% extends 'portfolio.html' %} {% load static %} -{% block organization_content %} +{% block portfolio_content %}

Domains

{% include "includes/domains_table.html" with portfolio=portfolio %} {% endblock %} diff --git a/src/registrar/templates/organization_requests.html b/src/registrar/templates/portfolio_requests.html similarity index 88% rename from src/registrar/templates/organization_requests.html rename to src/registrar/templates/portfolio_requests.html index 29d8e589d..b95f03914 100644 --- a/src/registrar/templates/organization_requests.html +++ b/src/registrar/templates/portfolio_requests.html @@ -1,8 +1,8 @@ -{% extends 'organization.html' %} +{% extends 'portfolio.html' %} {% load static %} -{% block organization_content %} +{% block portfolio_content %}

Domain requests

{% comment %} diff --git a/src/registrar/templates/organization_sidebar.html b/src/registrar/templates/portfolio_sidebar.html similarity index 87% rename from src/registrar/templates/organization_sidebar.html rename to src/registrar/templates/portfolio_sidebar.html index d2d7cd306..94c4a0b16 100644 --- a/src/registrar/templates/organization_sidebar.html +++ b/src/registrar/templates/portfolio_sidebar.html @@ -5,14 +5,14 @@

{{ portfolio.organization_name }}

  • - {% url 'organization-domains' portfolio.id as url %} + {% url 'portfolio-domains' portfolio.id as url %} Domains
  • - {% url 'organization-domain-requests' portfolio.id as url %} + {% url 'portfolio-domain-requests' portfolio.id as url %} Domain requests diff --git a/src/registrar/views/organizations.py b/src/registrar/views/portfolios.py similarity index 85% rename from src/registrar/views/organizations.py rename to src/registrar/views/portfolios.py index 61b764d7e..5ecd5d1d0 100644 --- a/src/registrar/views/organizations.py +++ b/src/registrar/views/portfolios.py @@ -5,7 +5,7 @@ from django.contrib.auth.decorators import login_required @login_required -def organization_domains(request, portfolio_id): +def portfolio_domains(request, portfolio_id): context = {} if request.user.is_authenticated: @@ -17,11 +17,11 @@ def organization_domains(request, portfolio_id): portfolio = get_object_or_404(Portfolio, id=portfolio_id) context["portfolio"] = portfolio - return render(request, "organization_domains.html", context) + return render(request, "portfolio_domains.html", context) @login_required -def organization_domain_requests(request, portfolio_id): +def portfolio_domain_requests(request, portfolio_id): context = {} if request.user.is_authenticated: @@ -36,4 +36,4 @@ def organization_domain_requests(request, portfolio_id): # This controls the creation of a new domain request in the wizard request.session["new_request"] = True - return render(request, "organization_requests.html", context) + return render(request, "portfolio_requests.html", context) From aa652fac1bb2332ce5ac715e95c00a32c456d7e6 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 20 Jun 2024 16:43:57 -0400 Subject: [PATCH 13/28] updating tests from org to portfolio --- src/registrar/tests/test_views.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 2554bed01..5c8232d85 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -908,7 +908,7 @@ class UserProfileTests(TestWithUser, WebTest): self.assertContains(profile_page, "Your profile has been updated") -class OrganizationsTests(TestWithUser, WebTest): +class PortfoliosTests(TestWithUser, WebTest): """A series of tests that target the organizations""" # csrf checks do not work well with WebTest. @@ -939,33 +939,33 @@ class OrganizationsTests(TestWithUser, WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @less_console_noise_decorator - def test_middleware_redirects_to_organization_homepage(self): - """Tests that a user is redirected to the org homepage when organization_feature is on and + def test_middleware_redirects_to_portfolio_homepage(self): + """Tests that a user is redirected to the portfolio homepage when organization_feature is on and a portfolio belongs to the user, test for the special h1s which only exist in that version of the homepage""" self.app.set_user(self.user.username) with override_flag("organization_feature", active=True): - # This will redirect the user to the org page. + # This will redirect the user to the portfolio page. # Follow implicity checks if our redirect is working. - org_page = self.app.get(reverse("home")).follow() + portfolio_page = self.app.get(reverse("home")).follow() self._set_session_cookie() # Assert that we're on the right page - self.assertContains(org_page, self.portfolio.organization_name) + self.assertContains(portfolio_page, self.portfolio.organization_name) - self.assertContains(org_page, "

    Domains

    ") + self.assertContains(portfolio_page, "

    Domains

    ") @less_console_noise_decorator def test_no_redirect_when_org_flag_false(self): """No redirect so no follow, implicitely test for the presense of the h2 by looking up its id""" self.app.set_user(self.user.username) - org_page = self.app.get(reverse("home")) + home_page = self.app.get(reverse("home")) self._set_session_cookie() - self.assertNotContains(org_page, self.portfolio.organization_name) + self.assertNotContains(home_page, self.portfolio.organization_name) - self.assertContains(org_page, 'id="domain-requests-header"') + self.assertContains(home_page, 'id="domain-requests-header"') @less_console_noise_decorator def test_no_redirect_when_user_has_no_portfolios(self): @@ -974,9 +974,9 @@ class OrganizationsTests(TestWithUser, WebTest): self.portfolio.delete() self.app.set_user(self.user.username) with override_flag("organization_feature", active=True): - org_page = self.app.get(reverse("home")) + home_page = self.app.get(reverse("home")) self._set_session_cookie() - self.assertNotContains(org_page, self.portfolio.organization_name) + self.assertNotContains(home_page, self.portfolio.organization_name) - self.assertContains(org_page, 'id="domain-requests-header"') + self.assertContains(home_page, 'id="domain-requests-header"') From 8b6f555fc48e02b5c700916c2722a610e5e76586 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 20 Jun 2024 16:48:31 -0400 Subject: [PATCH 14/28] formatted for code readability --- src/registrar/registrar_middleware.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 880f9ee40..f5274c99f 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -150,8 +150,6 @@ class CheckPortfolioMiddleware: user_portfolios = Portfolio.objects.filter(creator=request.user) if user_portfolios.exists(): first_portfolio = user_portfolios.first() - home_with_portfolio = reverse( - "portfolio-domains", kwargs={"portfolio_id": first_portfolio.id} - ) + home_with_portfolio = reverse("portfolio-domains", kwargs={"portfolio_id": first_portfolio.id}) return HttpResponseRedirect(home_with_portfolio) return None From 62ab9bb7cfc99abb276522b63e44825dc72c03b7 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 20 Jun 2024 14:49:36 -0600 Subject: [PATCH 15/28] linted --- src/registrar/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8d53545cf..f59258e18 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2036,7 +2036,7 @@ class DomainInformationInline(admin.StackedInline): def get_readonly_fields(self, request, obj=None): return DomainInformationAdmin.get_readonly_fields(self, request, obj=None) - + # Re-route the get_fieldsets method to utilize DomainInformationAdmin.get_fieldsets # since that has all the logic for excluding certain fields according to user permissions. # Then modify the remaining fields to further trim out any we don't want for this inline @@ -2045,7 +2045,7 @@ class DomainInformationInline(admin.StackedInline): # Grab fieldsets from DomainInformationAdmin so that it handles all logic # for permission-based field visibility. modified_fieldsets = DomainInformationAdmin.get_fieldsets(self, request, obj=None) - + # remove .gov domain from fieldset for index, (title, f) in enumerate(modified_fieldsets): if title == ".gov domain": From 8d8e2be9596b4f785bcde0d3ba5c020d6b0aede6 Mon Sep 17 00:00:00 2001 From: Rachid Mrad <107004823+rachidatecs@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:00:42 -0400 Subject: [PATCH 16/28] Update src/registrar/assets/js/get-gov.js Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- src/registrar/assets/js/get-gov.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 45f37a082..e6ae0927a 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1357,7 +1357,6 @@ document.addEventListener('DOMContentLoaded', function() { // which will cause bad delete requests to be sent. const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]'); preExistingModalPlaceholders.forEach(element => { - console.log('found one'); element.remove(); }); From 2995792375830a4b6247059d5473ca48df31b627 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:19:59 -0700 Subject: [PATCH 17/28] Add DomainGroup and Suborg models --- src/registrar/admin.py | 23 +++++++++++ .../0105_suborganization_domaingroup.py | 41 +++++++++++++++++++ src/registrar/models/__init__.py | 6 +++ src/registrar/models/domain_group.py | 33 +++++++++++++++ src/registrar/models/portfolio.py | 3 ++ src/registrar/models/suborganization.py | 23 +++++++++++ 6 files changed, 129 insertions(+) create mode 100644 src/registrar/migrations/0105_suborganization_domaingroup.py create mode 100644 src/registrar/models/domain_group.py create mode 100644 src/registrar/models/suborganization.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 215239d66..c0f52c696 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2655,6 +2655,27 @@ class WaffleFlagAdmin(FlagAdmin): model = models.WaffleFlag fields = "__all__" +class DomainGroupResource(resources.ModelResource): + """defines how each field in the referenced model should be mapped to the corresponding fields in the + import/export file""" + + class Meta: + model = models.DomainGroup + +class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin): + resource_classes = [DomainGroupResource] + list_display = ["name", "portfolio"] + +class SuborganizationResource(resources.ModelResource): + """defines how each field in the referenced model should be mapped to the corresponding fields in the + import/export file""" + + class Meta: + model = models.Suborganization + +class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): + resource_classes = [SuborganizationResource] + list_display = ["name", "portfolio"] admin.site.unregister(LogEntry) # Unregister the default registration @@ -2679,6 +2700,8 @@ admin.site.register(models.DomainRequest, DomainRequestAdmin) admin.site.register(models.TransitionDomain, TransitionDomainAdmin) admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) admin.site.register(models.Portfolio, PortfolioAdmin) +admin.site.register(models.DomainGroup, DomainGroupAdmin) +admin.site.register(models.Suborganization, SuborganizationAdmin) # Register our custom waffle implementations admin.site.register(models.WaffleFlag, WaffleFlagAdmin) diff --git a/src/registrar/migrations/0105_suborganization_domaingroup.py b/src/registrar/migrations/0105_suborganization_domaingroup.py new file mode 100644 index 000000000..3d99a4001 --- /dev/null +++ b/src/registrar/migrations/0105_suborganization_domaingroup.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.10 on 2024-06-20 21:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0104_create_groups_v13"), + ] + + operations = [ + migrations.CreateModel( + name="Suborganization", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(blank=True, help_text="Domain group", null=True, unique=True)), + ("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="DomainGroup", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(blank=True, help_text="Domain group", null=True, unique=True)), + ("domains", models.ManyToManyField(blank=True, to="registrar.domaininformation")), + ("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")), + ], + options={ + "unique_together": {("name", "portfolio")}, + }, + ), + ] diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index b2cffaf32..2af89ce00 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -17,6 +17,8 @@ from .transition_domain import TransitionDomain from .verified_by_staff import VerifiedByStaff from .waffle_flag import WaffleFlag from .portfolio import Portfolio +from .domain_group import DomainGroup +from .suborganization import Suborganization __all__ = [ @@ -38,6 +40,8 @@ __all__ = [ "VerifiedByStaff", "WaffleFlag", "Portfolio", + "DomainGroup", + "Suborganization" ] auditlog.register(Contact) @@ -58,3 +62,5 @@ auditlog.register(TransitionDomain) auditlog.register(VerifiedByStaff) auditlog.register(WaffleFlag) auditlog.register(Portfolio) +auditlog.register(DomainGroup) +auditlog.register(Suborganization) diff --git a/src/registrar/models/domain_group.py b/src/registrar/models/domain_group.py new file mode 100644 index 000000000..aa6d64e1f --- /dev/null +++ b/src/registrar/models/domain_group.py @@ -0,0 +1,33 @@ +from django.db import models +from .utility.time_stamped_model import TimeStampedModel +from registrar.models.portfolio import Portfolio +from registrar.models.domain_information import DomainInformation + + +class DomainGroup(TimeStampedModel): + + class Meta: + unique_together = [("name", "portfolio")] + + """ + TODO: Write DomainGroup description + """ + name = models.CharField( + null=True, + blank=True, + unique=True, + help_text="Domain group", + ) + + portfolio = models.ForeignKey( + "registrar.Portfolio", + on_delete=models.PROTECT + ) + + domains = models.ManyToManyField( + "registrar.DomainInformation", + blank=True + ) + + def __str__(self) -> str: + return f"{self.name}" \ No newline at end of file diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index a05422960..0ea036bb7 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -97,3 +97,6 @@ class Portfolio(TimeStampedModel): verbose_name="security contact e-mail", max_length=320, ) + + def __str__(self) -> str: + return f"{self.organization_name}" diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py new file mode 100644 index 000000000..6ebae62e2 --- /dev/null +++ b/src/registrar/models/suborganization.py @@ -0,0 +1,23 @@ +from django.db import models +from .utility.time_stamped_model import TimeStampedModel +from registrar.models.portfolio import Portfolio + + +class Suborganization(TimeStampedModel): + """ + TODO: Write DomainGroup description + """ + name = models.CharField( + null=True, + blank=True, + unique=True, + help_text="Domain group", + ) + + portfolio = models.ForeignKey( + "registrar.Portfolio", + on_delete=models.PROTECT, + ) + + def __str__(self) -> str: + return f"{self.name}" From b48802e0d7cfe73b2b7f44bb015c28145e4996b6 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:21:57 -0700 Subject: [PATCH 18/28] Change suborganization name help text --- src/registrar/migrations/0105_suborganization_domaingroup.py | 4 ++-- src/registrar/models/suborganization.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/migrations/0105_suborganization_domaingroup.py b/src/registrar/migrations/0105_suborganization_domaingroup.py index 3d99a4001..95d16e3b4 100644 --- a/src/registrar/migrations/0105_suborganization_domaingroup.py +++ b/src/registrar/migrations/0105_suborganization_domaingroup.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-06-20 21:18 +# Generated by Django 4.2.10 on 2024-06-20 21:21 from django.db import migrations, models import django.db.models.deletion @@ -17,7 +17,7 @@ class Migration(migrations.Migration): ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), - ("name", models.CharField(blank=True, help_text="Domain group", null=True, unique=True)), + ("name", models.CharField(blank=True, help_text="Suborganization", null=True, unique=True)), ("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")), ], options={ diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py index 6ebae62e2..15612e7d2 100644 --- a/src/registrar/models/suborganization.py +++ b/src/registrar/models/suborganization.py @@ -11,7 +11,7 @@ class Suborganization(TimeStampedModel): null=True, blank=True, unique=True, - help_text="Domain group", + help_text="Suborganization", ) portfolio = models.ForeignKey( From 0a80cb1c62b68c0dbccfbc237dd98ffee4caf0ad Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:42:27 -0700 Subject: [PATCH 19/28] Update descriptions of DomainGroup and Suborg --- src/registrar/models/domain_group.py | 2 +- src/registrar/models/suborganization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain_group.py b/src/registrar/models/domain_group.py index aa6d64e1f..f43627251 100644 --- a/src/registrar/models/domain_group.py +++ b/src/registrar/models/domain_group.py @@ -10,7 +10,7 @@ class DomainGroup(TimeStampedModel): unique_together = [("name", "portfolio")] """ - TODO: Write DomainGroup description + Organized group of domains. """ name = models.CharField( null=True, diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py index 15612e7d2..c90a76f27 100644 --- a/src/registrar/models/suborganization.py +++ b/src/registrar/models/suborganization.py @@ -5,7 +5,7 @@ from registrar.models.portfolio import Portfolio class Suborganization(TimeStampedModel): """ - TODO: Write DomainGroup description + Suborganization under an organization (portfolio) """ name = models.CharField( null=True, From a2a43b66b4a394bd8683178584e2241419125483 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:43:22 -0700 Subject: [PATCH 20/28] Fix lint errors --- src/registrar/admin.py | 5 +++++ src/registrar/models/__init__.py | 2 +- src/registrar/models/domain_group.py | 12 +++--------- src/registrar/models/suborganization.py | 1 + 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index c0f52c696..b1dcb1f6e 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2655,6 +2655,7 @@ class WaffleFlagAdmin(FlagAdmin): model = models.WaffleFlag fields = "__all__" + class DomainGroupResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file""" @@ -2662,10 +2663,12 @@ class DomainGroupResource(resources.ModelResource): class Meta: model = models.DomainGroup + class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin): resource_classes = [DomainGroupResource] list_display = ["name", "portfolio"] + class SuborganizationResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file""" @@ -2673,10 +2676,12 @@ class SuborganizationResource(resources.ModelResource): class Meta: model = models.Suborganization + class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): resource_classes = [SuborganizationResource] list_display = ["name", "portfolio"] + admin.site.unregister(LogEntry) # Unregister the default registration admin.site.register(LogEntry, CustomLogEntryAdmin) diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 2af89ce00..376739826 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -41,7 +41,7 @@ __all__ = [ "WaffleFlag", "Portfolio", "DomainGroup", - "Suborganization" + "Suborganization", ] auditlog.register(Contact) diff --git a/src/registrar/models/domain_group.py b/src/registrar/models/domain_group.py index f43627251..7f77cdc16 100644 --- a/src/registrar/models/domain_group.py +++ b/src/registrar/models/domain_group.py @@ -19,15 +19,9 @@ class DomainGroup(TimeStampedModel): help_text="Domain group", ) - portfolio = models.ForeignKey( - "registrar.Portfolio", - on_delete=models.PROTECT - ) + portfolio = models.ForeignKey("registrar.Portfolio", on_delete=models.PROTECT) - domains = models.ManyToManyField( - "registrar.DomainInformation", - blank=True - ) + domains = models.ManyToManyField("registrar.DomainInformation", blank=True) def __str__(self) -> str: - return f"{self.name}" \ No newline at end of file + return f"{self.name}" diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py index c90a76f27..c731c64b3 100644 --- a/src/registrar/models/suborganization.py +++ b/src/registrar/models/suborganization.py @@ -7,6 +7,7 @@ class Suborganization(TimeStampedModel): """ Suborganization under an organization (portfolio) """ + name = models.CharField( null=True, blank=True, From b8588b326628e2c3bc1e6b506f04f462aa12cec4 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:46:24 -0700 Subject: [PATCH 21/28] Remove unused imports --- src/registrar/models/domain_group.py | 2 -- src/registrar/models/suborganization.py | 1 - 2 files changed, 3 deletions(-) diff --git a/src/registrar/models/domain_group.py b/src/registrar/models/domain_group.py index 7f77cdc16..61bc90661 100644 --- a/src/registrar/models/domain_group.py +++ b/src/registrar/models/domain_group.py @@ -1,7 +1,5 @@ from django.db import models from .utility.time_stamped_model import TimeStampedModel -from registrar.models.portfolio import Portfolio -from registrar.models.domain_information import DomainInformation class DomainGroup(TimeStampedModel): diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py index c731c64b3..c96ea19f3 100644 --- a/src/registrar/models/suborganization.py +++ b/src/registrar/models/suborganization.py @@ -1,6 +1,5 @@ from django.db import models from .utility.time_stamped_model import TimeStampedModel -from registrar.models.portfolio import Portfolio class Suborganization(TimeStampedModel): From f0756683add2878bd75eaeb467f4b6ac29ab3f3d Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:06:11 -0700 Subject: [PATCH 22/28] Create new user groups migration for DomainGroup and Suborg --- .../migrations/0106_create_groups_v14.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/registrar/migrations/0106_create_groups_v14.py diff --git a/src/registrar/migrations/0106_create_groups_v14.py b/src/registrar/migrations/0106_create_groups_v14.py new file mode 100644 index 000000000..0ce3bafa5 --- /dev/null +++ b/src/registrar/migrations/0106_create_groups_v14.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0079 (which populates federal agencies) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0103_portfolio_domaininformation_portfolio_and_more"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] From c220a8a9ac74679d6f73803cba834207ae845243 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:10:19 -0700 Subject: [PATCH 23/28] Correct usergroup migration dependency --- src/registrar/migrations/0106_create_groups_v14.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/migrations/0106_create_groups_v14.py b/src/registrar/migrations/0106_create_groups_v14.py index 0ce3bafa5..816a49ac8 100644 --- a/src/registrar/migrations/0106_create_groups_v14.py +++ b/src/registrar/migrations/0106_create_groups_v14.py @@ -25,7 +25,7 @@ def create_groups(apps, schema_editor) -> Any: class Migration(migrations.Migration): dependencies = [ - ("registrar", "0103_portfolio_domaininformation_portfolio_and_more"), + ("registrar", "0105_suborganization_domaingroup"), ] operations = [ From 53991c127c2892dc2565bbb03ab69f20194055b3 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:39:12 -0700 Subject: [PATCH 24/28] Remove blank/null option and add max length to Suborg --- src/registrar/models/suborganization.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py index c96ea19f3..b1e010953 100644 --- a/src/registrar/models/suborganization.py +++ b/src/registrar/models/suborganization.py @@ -8,9 +8,8 @@ class Suborganization(TimeStampedModel): """ name = models.CharField( - null=True, - blank=True, unique=True, + max_length=1000, help_text="Suborganization", ) From 5712596d19e6a115d1955f4c6a261c9628931ac8 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:06:25 -0700 Subject: [PATCH 25/28] Remove blank and null options from Domain Group --- src/registrar/models/domain_group.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/models/domain_group.py b/src/registrar/models/domain_group.py index 61bc90661..8699c0337 100644 --- a/src/registrar/models/domain_group.py +++ b/src/registrar/models/domain_group.py @@ -11,8 +11,6 @@ class DomainGroup(TimeStampedModel): Organized group of domains. """ name = models.CharField( - null=True, - blank=True, unique=True, help_text="Domain group", ) From f60b79e13baac166241d6e26a4b856fd15a2d0a5 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:15:55 -0700 Subject: [PATCH 26/28] Readd migrations --- .../migrations/0105_suborganization_domaingroup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/migrations/0105_suborganization_domaingroup.py b/src/registrar/migrations/0105_suborganization_domaingroup.py index 95d16e3b4..471f9379c 100644 --- a/src/registrar/migrations/0105_suborganization_domaingroup.py +++ b/src/registrar/migrations/0105_suborganization_domaingroup.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-06-20 21:21 +# Generated by Django 4.2.10 on 2024-06-21 18:15 from django.db import migrations, models import django.db.models.deletion @@ -17,7 +17,7 @@ class Migration(migrations.Migration): ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), - ("name", models.CharField(blank=True, help_text="Suborganization", null=True, unique=True)), + ("name", models.CharField(help_text="Suborganization", max_length=1000, unique=True)), ("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")), ], options={ @@ -30,7 +30,7 @@ class Migration(migrations.Migration): ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), - ("name", models.CharField(blank=True, help_text="Domain group", null=True, unique=True)), + ("name", models.CharField(help_text="Domain group", unique=True)), ("domains", models.ManyToManyField(blank=True, to="registrar.domaininformation")), ("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")), ], From 60bbf5f0fc0d2885df75154d8d899f11ff9a5e87 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:44:09 -0700 Subject: [PATCH 27/28] Reorder description comment for domain group --- src/registrar/models/domain_group.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/models/domain_group.py b/src/registrar/models/domain_group.py index 8699c0337..16b1fd203 100644 --- a/src/registrar/models/domain_group.py +++ b/src/registrar/models/domain_group.py @@ -3,13 +3,13 @@ from .utility.time_stamped_model import TimeStampedModel class DomainGroup(TimeStampedModel): + """ + Organized group of domains. + """ class Meta: unique_together = [("name", "portfolio")] - """ - Organized group of domains. - """ name = models.CharField( unique=True, help_text="Domain group", From 69b81ba6d6803583024b282b872891f16ca3edde Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Mon, 24 Jun 2024 10:28:28 -0700 Subject: [PATCH 28/28] Remove import/export resource for domain group and suborg admin classes --- src/registrar/admin.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index b1dcb1f6e..686545f77 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2656,29 +2656,11 @@ class WaffleFlagAdmin(FlagAdmin): fields = "__all__" -class DomainGroupResource(resources.ModelResource): - """defines how each field in the referenced model should be mapped to the corresponding fields in the - import/export file""" - - class Meta: - model = models.DomainGroup - - class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin): - resource_classes = [DomainGroupResource] list_display = ["name", "portfolio"] -class SuborganizationResource(resources.ModelResource): - """defines how each field in the referenced model should be mapped to the corresponding fields in the - import/export file""" - - class Meta: - model = models.Suborganization - - class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): - resource_classes = [SuborganizationResource] list_display = ["name", "portfolio"]