From d32d94c5489caac80474ad9b00eb46d76adf6d91 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Apr 2024 14:06:13 -0600 Subject: [PATCH 01/76] Display --- src/registrar/assets/sass/_theme/_admin.scss | 8 +++++++ .../templates/admin/change_list.html | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index f5717d067..ad627e106 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -617,3 +617,11 @@ address.dja-address-contact-list { .usa-button__small-text { font-size: small; } + +p.django-admin__model-description{ + color: var(--primary); + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} diff --git a/src/registrar/templates/admin/change_list.html b/src/registrar/templates/admin/change_list.html index 05c2d4e64..78fa69fb5 100644 --- a/src/registrar/templates/admin/change_list.html +++ b/src/registrar/templates/admin/change_list.html @@ -2,6 +2,28 @@ {% block content_title %}

{{ title }}

+ + {# Adds a model description #} + {# TODO - this should be stored in context or something, not here. Make this a function. #} + {% if opts.model_name == 'domainrequest' %} +

+ This table contains all domain requests that have been started within the registrar and the status of those requests. + Updating values here will immediately update the corresponding values that users see in the registrar. + Once a domain request has been adjudicated, the details of that request should not be modified. + To update attributes (like an organization’s name) after a domain’s approval, go to Domains. + Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request. +

+ {% elif opts.model_name == 'contact' %} +

+ Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, + including other employees and authorizing officials. + Only contacts who have access to the registrar will have a corresponding record within the Users table. +
+ Updating someone’s contact information here will not affect that person’s Login.gov information. +

+ {% elif opts.model_name == 'domaininformation' %} + {% endif %} +

{{ cl.result_count }} {% if cl.get_ordering_field_columns %} From 65363f4e225a15c909abaf660ecf6758e66b606c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Apr 2024 14:39:27 -0600 Subject: [PATCH 02/76] Add initial architecture --- src/registrar/admin.py | 43 +++++++++++++++++++ .../templates/admin/change_list.html | 22 ++-------- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 42d73f10d..e2e3fb35b 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -382,6 +382,49 @@ class AdminSortFields: class AuditedAdmin(admin.ModelAdmin): """Custom admin to make auditing easier.""" + # TODO - move this into a helper somewhere, maybe an enum? + model_descriptions = { + 'domainrequest': ( + "This table contains all domain requests that have been started within the registrar and the " + "status of those requests. " + "Updating values here will immediately update the corresponding values that users see in the registrar. " + "Once a domain request has been adjudicated, the details of that request should not be modified. " + "To update attributes (like an organization’s name) after a domain’s approval, go to Domains. " + "Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request." + ), + "contact": ( + "Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, " + "including other employees and authorizing officials. " + "Only contacts who have access to the registrar will have a corresponding record within the " + "Users table." + # TODO - change this + "

" + "Updating someone’s contact information here will not affect that person’s Login.gov information." + ), + "domaininformation": ( + "Domain information represents the basic metadata for an approved domain and " + "the organization that manages it. " + "It does not include any information specific to the registry (DNS name servers, security email). " + "Registry-related information can be managed within the Domains table. " + "Updating values here will immediately update the corresponding values that users see in the registrar." + # TODO - change this + "

" + "Domain information is similar to Domain requests, and the fields are nearly identical," + "but edits made to one are not made to the other." + "Domain information exists so we don’t modify details of an approved request after " + "adjudication (since a domain request should be maintained as-adjudicated for records retention purposes). " + "Entries are created here upon approval of a domain request. " + ) + } + + def changelist_view(self, request, extra_context=None): + extra_context = extra_context or {} + model_name = self.model._meta.model_name + if model_name in self.model_descriptions: + extra_context['model_description'] = self.model_descriptions[model_name] + + return super().changelist_view(request, extra_context=extra_context) + def history_view(self, request, object_id, extra_context=None): """On clicking 'History', take admin to the auditlog view for an object.""" return HttpResponseRedirect( diff --git a/src/registrar/templates/admin/change_list.html b/src/registrar/templates/admin/change_list.html index 78fa69fb5..ab9797c9b 100644 --- a/src/registrar/templates/admin/change_list.html +++ b/src/registrar/templates/admin/change_list.html @@ -4,25 +4,9 @@

{{ title }}

{# Adds a model description #} - {# TODO - this should be stored in context or something, not here. Make this a function. #} - {% if opts.model_name == 'domainrequest' %} -

- This table contains all domain requests that have been started within the registrar and the status of those requests. - Updating values here will immediately update the corresponding values that users see in the registrar. - Once a domain request has been adjudicated, the details of that request should not be modified. - To update attributes (like an organization’s name) after a domain’s approval, go to Domains. - Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request. -

- {% elif opts.model_name == 'contact' %} -

- Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, - including other employees and authorizing officials. - Only contacts who have access to the registrar will have a corresponding record within the Users table. -
- Updating someone’s contact information here will not affect that person’s Login.gov information. -

- {% elif opts.model_name == 'domaininformation' %} - {% endif %} +

+ {{ model_description|safe }} +

{{ cl.result_count }} From 4bcd482e1b9f9f72bb4db3e7cddf91dfc5c92ab8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Apr 2024 08:39:51 -0600 Subject: [PATCH 03/76] Add content --- .../templates/admin/change_list.html | 153 +++++++++++++++++- 1 file changed, 152 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/admin/change_list.html b/src/registrar/templates/admin/change_list.html index ab9797c9b..0a4414e06 100644 --- a/src/registrar/templates/admin/change_list.html +++ b/src/registrar/templates/admin/change_list.html @@ -5,7 +5,158 @@ {# Adds a model description #}

- {{ model_description|safe }} + {% if opts.model_name == 'domainrequest' %} + This table contains all domain requests that have been started within the registrar and the status of those requests. + Updating values here will immediately update the corresponding values that users see in the registrar. + + Once a domain request has been adjudicated, the details of that request should not be modified. + To update attributes (like an organization’s name) after a domain’s approval, + go to Domains. + Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request. + {% elif opts.model_name == 'domaininformation' %} + Domain information represents the basic metadata for an approved domain and the organization that manages it. + It does not include any information specific to the registry (DNS name servers, security email). + Registry-related information + can be managed within the Domains table. + + + {# Acts as a
#} +

+ Updating values here will immediately update the corresponding values that users see in the registrar. + + {# Acts as a
#} +
+ Domain information is similar to Domain requests, + and the fields are nearly identical, + but edits made to one are not made to the other. + Domain information exists so we don’t modify details of an approved request after adjudication + (since a domain request should be maintained as-adjudicated for records retention purposes). + Entries are created here upon approval of a domain request. + {% elif opts.model_name == 'contact' %} + Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, + including other employees and authorizing officials. + Only contacts who have access to the registrar will have + a corresponding record within the Users table. + Updating someone’s contact information here will not affect that person’s Login.gov information. + {% elif opts.model_name == 'auditlog' %} + Log entries represent instances where something was created, updated, or deleted within the registrar or /admin. + The table on this page is useful for searching actions across all records. + To understand what happened to an individual record (like a domain request), + it’s better to go to that specific page + (Domain requests) and click on the “History” button. + {% elif opts.model_name == 'domain'%} + This table contains all approved domains in the .gov registrar. + In other words, with the exception of domains in the “Unknown”, ”On hold”, and “Deleted” states, + this table matches the list of domains in the .gov zone file. + + {# Acts as a
#} +
+ Individual domain pages allow you to view registry-related details and edit the associated domain information. + + {# Acts as a
#} +
+ Other actions available on the domain page include the ability to: + + + {# Acts as a
#} +
+ To view a domain from a domain manager’s perspective, click the "Manage domain" button. + That will allow you to make changes (e.g., update DNS settings, invite people to manage the domain) + directly inside the registrar. + {% elif opts.model_name == 'draftdomain' %} + This table represents all “requested domains” that have been saved within a domain request form. + If a registrant changes the requested domain in their form, + the original name they listed and the new name will appear as separate records. + + {# Acts as a
#} +
+ This table does not include “alternative domains,” + which are housed in the Websites table. + {% elif opts.model_name == 'host' %} + Entries in the Hosts table indicate the relationship between an approved domain and a name server address + (and, if applicable, the IP address for a name server address). + + {# Acts as a
#} +
+ In general, you should not modify these values here. They should be updated directly inside the registrar. + To update a domain’s name servers and/or IP addresses, + in the registrar, go to the domain in Domains, + then click the "Manage domain" button. + {% elif opts.model_name == 'publiccontact' %} + Public contacts represent the three registry contact types (administrative, technical, and security) + and their fields that are exposed in WHOIS data. + + {# Acts as a
#} +
+ We don’t currently allow registrants to publish real contact information to WHOIS, + but we must publish something to WHOIS. For each of the contact types, we use default values that are + associated with the program instead of the real contact information, + which we then redact in whole at the registry/WHOIS. + We do allow registrants to set a security contact email address, + which is published to WHOIS when a user sets one. + + {# Acts as a
#} +
+ The public contacts in this table are a reflection of the data in the registry and should not be updated. + This information is primarily used by developers for validation purposes. + {% elif opts.model_name == 'transitiondomain' %} + This table represents the domains that were transitioned from the old registry in November 2023. + This data has been preserved for historical reference and should not be updated. + {% elif opts.model_name == 'userdomainrole' %} + This table represents the managers who are assigned to each domain in the registrar. + There are separate records for each domain/manager combination. + Managers can update information related to a domain, such as DNS data and security contact. + + {# Acts as a
#} +
+ The creator of an approved domain request automatically becomes a manager for that domain. + Anyone who retrieves a domain invitation is also assigned the manager role. + {% elif opts.model_name == 'usergroup' %} + Groups are a way to bundle admin permissions so they can be easily assigned to multiple users. + Once a group is created, it can be assigned to people via the Users table. + + {# Acts as a
#} +
+ To maintain a single source of truth across all environments (stable, staging, etc.), + the groups and permissions are set and updated through the codebase. + Do not add, edit or delete user groups here. + + {% elif opts.model_name == 'user' %} + A user is anyone who has access to the registrar. This includes request creators, domain managers, and CISA administrators. + Within each user record, you can review that user’s permissions and user status. + + {# Acts as a
#} +
+ If a user has a domain request in ineligible status, then their user status will be “restricted.” + Users who are in restricted status cannot create/edit domain requests or approved domains, + and their existing requests are locked. They will see a 403 error if they try to take action in the registrar. + + {# Acts as a
#} +
+ Each user record displays the associated “contact” info for that user, + which is the same info found in the Contacts table. + Updating these details on the user record will also update the corresponding contact record for that user. + {% elif opts.model_name == 'verifiedbystaff' %} + This table contains users who have been allowed to bypass identity proofing through Login.gov after approval by a CISA representative. + Bypassing identity proofing means they will not be asked to provide a form of ID, PII, and so on. + + {# Acts as a
#} +
+ Additions to this table should be rare, and only after obtaining confirmation of their identity directly (or from a trusted person). + Once a verified-by-staff user has been added as a domain manager, they can be removed from this list, + (However, if they are removed as a domain manager for all domains and they attempt to sign in again, they will be identity proofed by Login.gov). + {% elif opts.model_name == 'websites' %} + This section lists all the “current websites” and “alternative domains” that users have submitted in domain requests since January 2024. + {# Acts as a
#} +
+ This does not include any “requested domains” that have appeared within the Domain requests table. + Those names are managed in the Draft domains table. + {% endif %}

From 2223d496c19f214854e432cb419e89f90e36fa36 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Apr 2024 08:50:16 -0600 Subject: [PATCH 04/76] Cleanup --- src/registrar/assets/sass/_theme/_admin.scss | 2 +- .../templates/admin/change_list.html | 155 +-------------- .../templates/admin/model_descriptions.html | 183 ++++++++++++++++++ 3 files changed, 185 insertions(+), 155 deletions(-) create mode 100644 src/registrar/templates/admin/model_descriptions.html diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index ad627e106..137365309 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -618,7 +618,7 @@ address.dja-address-contact-list { font-size: small; } -p.django-admin__model-description{ +div.django-admin__model-description{ color: var(--primary); display: -webkit-box; -webkit-line-clamp: 3; diff --git a/src/registrar/templates/admin/change_list.html b/src/registrar/templates/admin/change_list.html index 0a4414e06..43abf0861 100644 --- a/src/registrar/templates/admin/change_list.html +++ b/src/registrar/templates/admin/change_list.html @@ -4,160 +4,7 @@

{{ title }}

{# Adds a model description #} -

- {% if opts.model_name == 'domainrequest' %} - This table contains all domain requests that have been started within the registrar and the status of those requests. - Updating values here will immediately update the corresponding values that users see in the registrar. - - Once a domain request has been adjudicated, the details of that request should not be modified. - To update attributes (like an organization’s name) after a domain’s approval, - go to Domains. - Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request. - {% elif opts.model_name == 'domaininformation' %} - Domain information represents the basic metadata for an approved domain and the organization that manages it. - It does not include any information specific to the registry (DNS name servers, security email). - Registry-related information - can be managed within the Domains table. - - - {# Acts as a
#} -

- Updating values here will immediately update the corresponding values that users see in the registrar. - - {# Acts as a
#} -
- Domain information is similar to Domain requests, - and the fields are nearly identical, - but edits made to one are not made to the other. - Domain information exists so we don’t modify details of an approved request after adjudication - (since a domain request should be maintained as-adjudicated for records retention purposes). - Entries are created here upon approval of a domain request. - {% elif opts.model_name == 'contact' %} - Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, - including other employees and authorizing officials. - Only contacts who have access to the registrar will have - a corresponding record within the Users table. - Updating someone’s contact information here will not affect that person’s Login.gov information. - {% elif opts.model_name == 'auditlog' %} - Log entries represent instances where something was created, updated, or deleted within the registrar or /admin. - The table on this page is useful for searching actions across all records. - To understand what happened to an individual record (like a domain request), - it’s better to go to that specific page - (Domain requests) and click on the “History” button. - {% elif opts.model_name == 'domain'%} - This table contains all approved domains in the .gov registrar. - In other words, with the exception of domains in the “Unknown”, ”On hold”, and “Deleted” states, - this table matches the list of domains in the .gov zone file. - - {# Acts as a
#} -
- Individual domain pages allow you to view registry-related details and edit the associated domain information. - - {# Acts as a
#} -
- Other actions available on the domain page include the ability to: - - - {# Acts as a
#} -
- To view a domain from a domain manager’s perspective, click the "Manage domain" button. - That will allow you to make changes (e.g., update DNS settings, invite people to manage the domain) - directly inside the registrar. - {% elif opts.model_name == 'draftdomain' %} - This table represents all “requested domains” that have been saved within a domain request form. - If a registrant changes the requested domain in their form, - the original name they listed and the new name will appear as separate records. - - {# Acts as a
#} -
- This table does not include “alternative domains,” - which are housed in the Websites table. - {% elif opts.model_name == 'host' %} - Entries in the Hosts table indicate the relationship between an approved domain and a name server address - (and, if applicable, the IP address for a name server address). - - {# Acts as a
#} -
- In general, you should not modify these values here. They should be updated directly inside the registrar. - To update a domain’s name servers and/or IP addresses, - in the registrar, go to the domain in Domains, - then click the "Manage domain" button. - {% elif opts.model_name == 'publiccontact' %} - Public contacts represent the three registry contact types (administrative, technical, and security) - and their fields that are exposed in WHOIS data. - - {# Acts as a
#} -
- We don’t currently allow registrants to publish real contact information to WHOIS, - but we must publish something to WHOIS. For each of the contact types, we use default values that are - associated with the program instead of the real contact information, - which we then redact in whole at the registry/WHOIS. - We do allow registrants to set a security contact email address, - which is published to WHOIS when a user sets one. - - {# Acts as a
#} -
- The public contacts in this table are a reflection of the data in the registry and should not be updated. - This information is primarily used by developers for validation purposes. - {% elif opts.model_name == 'transitiondomain' %} - This table represents the domains that were transitioned from the old registry in November 2023. - This data has been preserved for historical reference and should not be updated. - {% elif opts.model_name == 'userdomainrole' %} - This table represents the managers who are assigned to each domain in the registrar. - There are separate records for each domain/manager combination. - Managers can update information related to a domain, such as DNS data and security contact. - - {# Acts as a
#} -
- The creator of an approved domain request automatically becomes a manager for that domain. - Anyone who retrieves a domain invitation is also assigned the manager role. - {% elif opts.model_name == 'usergroup' %} - Groups are a way to bundle admin permissions so they can be easily assigned to multiple users. - Once a group is created, it can be assigned to people via the Users table. - - {# Acts as a
#} -
- To maintain a single source of truth across all environments (stable, staging, etc.), - the groups and permissions are set and updated through the codebase. - Do not add, edit or delete user groups here. - - {% elif opts.model_name == 'user' %} - A user is anyone who has access to the registrar. This includes request creators, domain managers, and CISA administrators. - Within each user record, you can review that user’s permissions and user status. - - {# Acts as a
#} -
- If a user has a domain request in ineligible status, then their user status will be “restricted.” - Users who are in restricted status cannot create/edit domain requests or approved domains, - and their existing requests are locked. They will see a 403 error if they try to take action in the registrar. - - {# Acts as a
#} -
- Each user record displays the associated “contact” info for that user, - which is the same info found in the Contacts table. - Updating these details on the user record will also update the corresponding contact record for that user. - {% elif opts.model_name == 'verifiedbystaff' %} - This table contains users who have been allowed to bypass identity proofing through Login.gov after approval by a CISA representative. - Bypassing identity proofing means they will not be asked to provide a form of ID, PII, and so on. - - {# Acts as a
#} -
- Additions to this table should be rare, and only after obtaining confirmation of their identity directly (or from a trusted person). - Once a verified-by-staff user has been added as a domain manager, they can be removed from this list, - (However, if they are removed as a domain manager for all domains and they attempt to sign in again, they will be identity proofed by Login.gov). - {% elif opts.model_name == 'websites' %} - This section lists all the “current websites” and “alternative domains” that users have submitted in domain requests since January 2024. - {# Acts as a
#} -
- This does not include any “requested domains” that have appeared within the Domain requests table. - Those names are managed in the Draft domains table. - {% endif %} -

+ {% include "admin/model_descriptions.html" %}

{{ cl.result_count }} diff --git a/src/registrar/templates/admin/model_descriptions.html b/src/registrar/templates/admin/model_descriptions.html new file mode 100644 index 000000000..9c311ce5c --- /dev/null +++ b/src/registrar/templates/admin/model_descriptions.html @@ -0,0 +1,183 @@ +
+ {% if opts.model_name == 'domainrequest' %} +

+ This table contains all domain requests that have been started within the registrar and the status of those requests. + Updating values here will immediately update the corresponding values that users see in the registrar. +

+ +

+ Once a domain request has been adjudicated, the details of that request should not be modified. + To update attributes (like an organization’s name) after a domain’s approval, + go to Domains. + Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request. +

+ {% elif opts.model_name == 'domaininformation' %} +

+ Domain information represents the basic metadata for an approved domain and the organization that manages it. + It does not include any information specific to the registry (DNS name servers, security email). + Registry-related information + can be managed within the Domains table. +

+ +

+ Updating values here will immediately update the corresponding values that users see in the registrar. +

+ +

+ Domain information is similar to Domain requests, + and the fields are nearly identical, + but edits made to one are not made to the other. + Domain information exists so we don’t modify details of an approved request after adjudication + (since a domain request should be maintained as-adjudicated for records retention purposes). + Entries are created here upon approval of a domain request. +

+ {% elif opts.model_name == 'contact' %} +

+ Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, + including other employees and authorizing officials. + Only contacts who have access to the registrar will have + a corresponding record within the Users table. + Updating someone’s contact information here will not affect that person’s Login.gov information. +

+ {% elif opts.model_name == 'auditlog' %} +

+ Log entries represent instances where something was created, updated, or deleted within the registrar or /admin. + The table on this page is useful for searching actions across all records. + To understand what happened to an individual record (like a domain request), + it’s better to go to that specific page + (Domain requests) and click on the “History” button. +

+ {% elif opts.model_name == 'domain'%} +

+ This table contains all approved domains in the .gov registrar. + In other words, with the exception of domains in the “Unknown”, ”On hold”, and “Deleted” states, + this table matches the list of domains in the .gov zone file. +

+ +

+ Individual domain pages allow you to view registry-related details and edit the associated domain information. +

+ +

+ Other actions available on the domain page include the ability to: +

+
    +
  • Extend a domain’s expiration date
  • +
  • Place a domain “On hold”
  • +
  • Remove a domain from the registry
  • +
  • View the domain from a domain manager’s perspective +
+ +

+ To view a domain from a domain manager’s perspective, click the "Manage domain" button. + That will allow you to make changes (e.g., update DNS settings, invite people to manage the domain) + directly inside the registrar. +

+ {% elif opts.model_name == 'draftdomain' %} +

+ This table represents all “requested domains” that have been saved within a domain request form. + If a registrant changes the requested domain in their form, + the original name they listed and the new name will appear as separate records. +

+ +

+ This table does not include “alternative domains,” + which are housed in the Websites table. +

+ {% elif opts.model_name == 'host' %} +

+ Entries in the Hosts table indicate the relationship between an approved domain and a name server address + (and, if applicable, the IP address for a name server address). +

+ +

+ In general, you should not modify these values here. They should be updated directly inside the registrar. + To update a domain’s name servers and/or IP addresses, + in the registrar, go to the domain in Domains, + then click the "Manage domain" button. +

+ {% elif opts.model_name == 'publiccontact' %} +

+ Public contacts represent the three registry contact types (administrative, technical, and security) + and their fields that are exposed in WHOIS data. +

+ +

+ We don’t currently allow registrants to publish real contact information to WHOIS, + but we must publish something to WHOIS. For each of the contact types, we use default values that are + associated with the program instead of the real contact information, + which we then redact in whole at the registry/WHOIS. + We do allow registrants to set a security contact email address, + which is published to WHOIS when a user sets one. +

+ +

+ The public contacts in this table are a reflection of the data in the registry and should not be updated. + This information is primarily used by developers for validation purposes. +

+ {% elif opts.model_name == 'transitiondomain' %} +

+ This table represents the domains that were transitioned from the old registry in November 2023. + This data has been preserved for historical reference and should not be updated. +

+ {% elif opts.model_name == 'userdomainrole' %} +

+ This table represents the managers who are assigned to each domain in the registrar. + There are separate records for each domain/manager combination. + Managers can update information related to a domain, such as DNS data and security contact. +

+ +

+ The creator of an approved domain request automatically becomes a manager for that domain. + Anyone who retrieves a domain invitation is also assigned the manager role. +

+ {% elif opts.model_name == 'usergroup' %} +

+ Groups are a way to bundle admin permissions so they can be easily assigned to multiple users. + Once a group is created, it can be assigned to people via the Users table. +

+ +

+ To maintain a single source of truth across all environments (stable, staging, etc.), + the groups and permissions are set and updated through the codebase. + Do not add, edit or delete user groups here. +

+ {% elif opts.model_name == 'user' %} +

+ A user is anyone who has access to the registrar. This includes request creators, domain managers, and CISA administrators. + Within each user record, you can review that user’s permissions and user status. +

+ +

+ If a user has a domain request in ineligible status, then their user status will be “restricted.” + Users who are in restricted status cannot create/edit domain requests or approved domains, + and their existing requests are locked. They will see a 403 error if they try to take action in the registrar. +

+ +

+ Each user record displays the associated “contact” info for that user, + which is the same info found in the Contacts table. + Updating these details on the user record will also update the corresponding contact record for that user. +

+ {% elif opts.model_name == 'verifiedbystaff' %} +

+ This table contains users who have been allowed to bypass identity proofing through Login.gov after approval by a CISA representative. + Bypassing identity proofing means they will not be asked to provide a form of ID, PII, and so on. +

+ +

+ Additions to this table should be rare, and only after obtaining confirmation of their identity directly (or from a trusted person). + Once a verified-by-staff user has been added as a domain manager, they can be removed from this list, + (However, if they are removed as a domain manager for all domains and they attempt to sign in again, they will be identity proofed by Login.gov). +

+ {% elif opts.model_name == 'websites' %} +

+ This section lists all the “current websites” and “alternative domains” that users have submitted in domain requests since January 2024. +

+ +

+ This does not include any “requested domains” that have appeared within the Domain requests table. + Those names are managed in the Draft domains table. +

+ {% endif %} +
From bc8a5cc0e7385fdcf1b0db8a0bdd81845ec3a503 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:15:12 -0600 Subject: [PATCH 05/76] CSS --- src/registrar/assets/sass/_theme/_admin.scss | 17 +- .../templates/admin/model_descriptions.html | 341 +++++++++--------- 2 files changed, 188 insertions(+), 170 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 137365309..6d49b724e 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -619,9 +619,22 @@ address.dja-address-contact-list { } div.django-admin__model-description{ - color: var(--primary); + p, li { + color: var(--primary); + font-size: medium; + line-height: 1.2em; + } + + li { + list-style-type: disc; + } + + button { + padding-top: 20px; + } + display: -webkit-box; - -webkit-line-clamp: 3; + -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } diff --git a/src/registrar/templates/admin/model_descriptions.html b/src/registrar/templates/admin/model_descriptions.html index 9c311ce5c..f1c580af4 100644 --- a/src/registrar/templates/admin/model_descriptions.html +++ b/src/registrar/templates/admin/model_descriptions.html @@ -1,183 +1,188 @@ -
- {% if opts.model_name == 'domainrequest' %} -

- This table contains all domain requests that have been started within the registrar and the status of those requests. - Updating values here will immediately update the corresponding values that users see in the registrar. -

+ +
+ {% if opts.model_name == 'domainrequest' %} +

+ This table contains all domain requests that have been started within the registrar and the status of those requests. + Updating values here will immediately update the corresponding values that users see in the registrar. +

-

- Once a domain request has been adjudicated, the details of that request should not be modified. - To update attributes (like an organization’s name) after a domain’s approval, - go to Domains. - Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request. -

- {% elif opts.model_name == 'domaininformation' %} -

- Domain information represents the basic metadata for an approved domain and the organization that manages it. - It does not include any information specific to the registry (DNS name servers, security email). - Registry-related information - can be managed within the Domains table. -

+

+ Once a domain request has been adjudicated, the details of that request should not be modified. + To update attributes (like an organization’s name) after a domain’s approval, + go to Domains. + Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request. +

+ {% elif opts.model_name == 'domaininformation' %} +

+ Domain information represents the basic metadata for an approved domain and the organization that manages it. + It does not include any information specific to the registry (DNS name servers, security email). + Registry-related information + can be managed within the Domains table. +

-

- Updating values here will immediately update the corresponding values that users see in the registrar. -

+

+ Updating values here will immediately update the corresponding values that users see in the registrar. +

-

- Domain information is similar to Domain requests, - and the fields are nearly identical, - but edits made to one are not made to the other. - Domain information exists so we don’t modify details of an approved request after adjudication - (since a domain request should be maintained as-adjudicated for records retention purposes). - Entries are created here upon approval of a domain request. -

- {% elif opts.model_name == 'contact' %} -

- Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, - including other employees and authorizing officials. - Only contacts who have access to the registrar will have - a corresponding record within the Users table. - Updating someone’s contact information here will not affect that person’s Login.gov information. -

- {% elif opts.model_name == 'auditlog' %} -

- Log entries represent instances where something was created, updated, or deleted within the registrar or /admin. - The table on this page is useful for searching actions across all records. - To understand what happened to an individual record (like a domain request), - it’s better to go to that specific page - (Domain requests) and click on the “History” button. -

- {% elif opts.model_name == 'domain'%} -

- This table contains all approved domains in the .gov registrar. - In other words, with the exception of domains in the “Unknown”, ”On hold”, and “Deleted” states, - this table matches the list of domains in the .gov zone file. -

+

+ Domain information is similar to Domain requests, + and the fields are nearly identical, + but edits made to one are not made to the other. + Domain information exists so we don’t modify details of an approved request after adjudication + (since a domain request should be maintained as-adjudicated for records retention purposes). + Entries are created here upon approval of a domain request. +

+ {% elif opts.model_name == 'contact' %} +

+ Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, + including other employees and authorizing officials. + Only contacts who have access to the registrar will have + a corresponding record within the Users table. + Updating someone’s contact information here will not affect that person’s Login.gov information. +

+ {% elif opts.model_name == 'auditlog' %} +

+ Log entries represent instances where something was created, updated, or deleted within the registrar or /admin. + The table on this page is useful for searching actions across all records. + To understand what happened to an individual record (like a domain request), + it’s better to go to that specific page + (Domain requests) and click on the “History” button. +

+ {% elif opts.model_name == 'domain'%} +

+ This table contains all approved domains in the .gov registrar. + In other words, with the exception of domains in the “Unknown”, ”On hold”, and “Deleted” states, + this table matches the list of domains in the .gov zone file. +

-

- Individual domain pages allow you to view registry-related details and edit the associated domain information. -

+

+ Individual domain pages allow you to view registry-related details and edit the associated domain information. +

-

- Other actions available on the domain page include the ability to: -

-
    -
  • Extend a domain’s expiration date
  • -
  • Place a domain “On hold”
  • -
  • Remove a domain from the registry
  • -
  • View the domain from a domain manager’s perspective -
+

+ Other actions available on the domain page include the ability to: +

+
    +
  • Extend a domain’s expiration date
  • +
  • Place a domain “On hold”
  • +
  • Remove a domain from the registry
  • +
  • View the domain from a domain manager’s perspective +
-

- To view a domain from a domain manager’s perspective, click the "Manage domain" button. - That will allow you to make changes (e.g., update DNS settings, invite people to manage the domain) - directly inside the registrar. -

- {% elif opts.model_name == 'draftdomain' %} -

- This table represents all “requested domains” that have been saved within a domain request form. - If a registrant changes the requested domain in their form, - the original name they listed and the new name will appear as separate records. -

- -

- This table does not include “alternative domains,” - which are housed in the Websites table. -

- {% elif opts.model_name == 'host' %} -

- Entries in the Hosts table indicate the relationship between an approved domain and a name server address - (and, if applicable, the IP address for a name server address). -

+

+ To view a domain from a domain manager’s perspective, click the "Manage domain" button. + That will allow you to make changes (e.g., update DNS settings, invite people to manage the domain) + directly inside the registrar. +

+ {% elif opts.model_name == 'draftdomain' %} +

+ This table represents all “requested domains” that have been saved within a domain request form. + If a registrant changes the requested domain in their form, + the original name they listed and the new name will appear as separate records. +

+ +

+ This table does not include “alternative domains,” + which are housed in the Websites table. +

+ {% elif opts.model_name == 'host' %} +

+ Entries in the Hosts table indicate the relationship between an approved domain and a name server address + (and, if applicable, the IP address for a name server address). +

-

- In general, you should not modify these values here. They should be updated directly inside the registrar. - To update a domain’s name servers and/or IP addresses, - in the registrar, go to the domain in Domains, - then click the "Manage domain" button. -

- {% elif opts.model_name == 'publiccontact' %} -

- Public contacts represent the three registry contact types (administrative, technical, and security) - and their fields that are exposed in WHOIS data. -

+

+ In general, you should not modify these values here. They should be updated directly inside the registrar. + To update a domain’s name servers and/or IP addresses, + in the registrar, go to the domain in Domains, + then click the "Manage domain" button. +

+ {% elif opts.model_name == 'publiccontact' %} +

+ Public contacts represent the three registry contact types (administrative, technical, and security) + and their fields that are exposed in WHOIS data. +

-

- We don’t currently allow registrants to publish real contact information to WHOIS, - but we must publish something to WHOIS. For each of the contact types, we use default values that are - associated with the program instead of the real contact information, - which we then redact in whole at the registry/WHOIS. - We do allow registrants to set a security contact email address, - which is published to WHOIS when a user sets one. -

+

+ We don’t currently allow registrants to publish real contact information to WHOIS, + but we must publish something to WHOIS. For each of the contact types, we use default values that are + associated with the program instead of the real contact information, + which we then redact in whole at the registry/WHOIS. + We do allow registrants to set a security contact email address, + which is published to WHOIS when a user sets one. +

-

- The public contacts in this table are a reflection of the data in the registry and should not be updated. - This information is primarily used by developers for validation purposes. -

- {% elif opts.model_name == 'transitiondomain' %} -

- This table represents the domains that were transitioned from the old registry in November 2023. - This data has been preserved for historical reference and should not be updated. -

- {% elif opts.model_name == 'userdomainrole' %} -

- This table represents the managers who are assigned to each domain in the registrar. - There are separate records for each domain/manager combination. - Managers can update information related to a domain, such as DNS data and security contact. -

+

+ The public contacts in this table are a reflection of the data in the registry and should not be updated. + This information is primarily used by developers for validation purposes. +

+ {% elif opts.model_name == 'transitiondomain' %} +

+ This table represents the domains that were transitioned from the old registry in November 2023. + This data has been preserved for historical reference and should not be updated. +

+ {% elif opts.model_name == 'userdomainrole' %} +

+ This table represents the managers who are assigned to each domain in the registrar. + There are separate records for each domain/manager combination. + Managers can update information related to a domain, such as DNS data and security contact. +

-

- The creator of an approved domain request automatically becomes a manager for that domain. - Anyone who retrieves a domain invitation is also assigned the manager role. -

- {% elif opts.model_name == 'usergroup' %} -

- Groups are a way to bundle admin permissions so they can be easily assigned to multiple users. - Once a group is created, it can be assigned to people via the Users table. -

+

+ The creator of an approved domain request automatically becomes a manager for that domain. + Anyone who retrieves a domain invitation is also assigned the manager role. +

+ {% elif opts.model_name == 'usergroup' %} +

+ Groups are a way to bundle admin permissions so they can be easily assigned to multiple users. + Once a group is created, it can be assigned to people via the Users table. +

-

- To maintain a single source of truth across all environments (stable, staging, etc.), - the groups and permissions are set and updated through the codebase. - Do not add, edit or delete user groups here. -

- {% elif opts.model_name == 'user' %} -

- A user is anyone who has access to the registrar. This includes request creators, domain managers, and CISA administrators. - Within each user record, you can review that user’s permissions and user status. -

+

+ To maintain a single source of truth across all environments (stable, staging, etc.), + the groups and permissions are set and updated through the codebase. + Do not add, edit or delete user groups here. +

+ {% elif opts.model_name == 'user' %} +

+ A user is anyone who has access to the registrar. This includes request creators, domain managers, and CISA administrators. + Within each user record, you can review that user’s permissions and user status. +

-

- If a user has a domain request in ineligible status, then their user status will be “restricted.” - Users who are in restricted status cannot create/edit domain requests or approved domains, - and their existing requests are locked. They will see a 403 error if they try to take action in the registrar. -

+

+ If a user has a domain request in ineligible status, then their user status will be “restricted.” + Users who are in restricted status cannot create/edit domain requests or approved domains, + and their existing requests are locked. They will see a 403 error if they try to take action in the registrar. +

-

- Each user record displays the associated “contact” info for that user, - which is the same info found in the Contacts table. - Updating these details on the user record will also update the corresponding contact record for that user. -

- {% elif opts.model_name == 'verifiedbystaff' %} -

- This table contains users who have been allowed to bypass identity proofing through Login.gov after approval by a CISA representative. - Bypassing identity proofing means they will not be asked to provide a form of ID, PII, and so on. -

+

+ Each user record displays the associated “contact” info for that user, + which is the same info found in the Contacts table. + Updating these details on the user record will also update the corresponding contact record for that user. +

+ {% elif opts.model_name == 'verifiedbystaff' %} +

+ This table contains users who have been allowed to bypass identity proofing through Login.gov after approval by a CISA representative. + Bypassing identity proofing means they will not be asked to provide a form of ID, PII, and so on. +

-

- Additions to this table should be rare, and only after obtaining confirmation of their identity directly (or from a trusted person). - Once a verified-by-staff user has been added as a domain manager, they can be removed from this list, - (However, if they are removed as a domain manager for all domains and they attempt to sign in again, they will be identity proofed by Login.gov). -

- {% elif opts.model_name == 'websites' %} -

- This section lists all the “current websites” and “alternative domains” that users have submitted in domain requests since January 2024. -

+

+ Additions to this table should be rare, and only after obtaining confirmation of their identity directly (or from a trusted person). + Once a verified-by-staff user has been added as a domain manager, they can be removed from this list, + (However, if they are removed as a domain manager for all domains and they attempt to sign in again, they will be identity proofed by Login.gov). +

+ {% elif opts.model_name == 'website' %} +

+ This table lists all the “current websites” and “alternative domains” that users have submitted in domain requests since January 2024. +

-

- This does not include any “requested domains” that have appeared within the Domain requests table. - Those names are managed in the Draft domains table. -

- {% endif %} -
+

+ This does not include any “requested domains” that have appeared within the Domain requests table. + Those names are managed in the Draft domains table. +

+ {% else %} +

This table does not have a description yet.

+ {% endif %} +
+ + \ No newline at end of file From b072e4f478f982b258163df5cddbc4cc2f8c9c3a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:30:14 -0600 Subject: [PATCH 06/76] Button styling --- src/registrar/assets/sass/_theme/_admin.scss | 3 ++- src/registrar/templates/admin/model_descriptions.html | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 6d49b724e..88c77cd09 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -630,7 +630,8 @@ div.django-admin__model-description{ } button { - padding-top: 20px; + margin-top: 25px !important; + height: 10px !important; } display: -webkit-box; diff --git a/src/registrar/templates/admin/model_descriptions.html b/src/registrar/templates/admin/model_descriptions.html index f1c580af4..d04fe9bbe 100644 --- a/src/registrar/templates/admin/model_descriptions.html +++ b/src/registrar/templates/admin/model_descriptions.html @@ -184,5 +184,5 @@

This table does not have a description yet.

{% endif %} - + \ No newline at end of file From 0bcc9ca43ac0153b06835bd98ebd83ccaa683b89 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Apr 2024 12:36:42 -0600 Subject: [PATCH 07/76] Add styling + javascript --- src/registrar/assets/js/get-gov-admin.js | 24 +++++++++++++++ src/registrar/assets/sass/_theme/_admin.scss | 29 ++++++++++++------- .../templates/admin/model_descriptions.html | 4 +-- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 2909a48be..3a5c9c184 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -555,3 +555,27 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, observer.observe(targetElement); } })(); + +/** An IIFE for toggling the overflow styles on django-admin__model-description (the show more / show less button) */ +(function () { + let toggleButton = document.getElementById('dja-show-more-model-description'); + let descriptionDiv = document.querySelector('.dja__model-description'); + + if (toggleButton && descriptionDiv) { + toggleButton.addEventListener('click', function() { + // Toggle the class on the description div + descriptionDiv.classList.toggle('dja__model-description--no-overflow'); + + // Change the button text based on the presence of the class + if (descriptionDiv.classList.contains('dja__model-description--no-overflow')) { + toggleButton.textContent = 'Show less'; + + // Move the div to where it was when the page first loaded + descriptionDiv.appendChild(toggleButton); + } else { + toggleButton.textContent = 'Show more'; + descriptionDiv.insertAdjacentElement('afterend', toggleButton); + } + }); + } +})(); \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 88c77cd09..898d3d538 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -618,7 +618,12 @@ address.dja-address-contact-list { font-size: small; } -div.django-admin__model-description{ +div.dja__model-description{ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + p, li { color: var(--primary); font-size: medium; @@ -629,13 +634,17 @@ div.django-admin__model-description{ list-style-type: disc; } - button { - margin-top: 25px !important; - height: 10px !important; - } - - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; +} + +.dja__model-description + button { + margin-top: 25px !important; + height: 10px !important; +} + +div.dja__model-description.dja__model-description--no-overflow { + display: block; + overflow: unset; + button { + margin-top: unset; + } } diff --git a/src/registrar/templates/admin/model_descriptions.html b/src/registrar/templates/admin/model_descriptions.html index d04fe9bbe..425c0e677 100644 --- a/src/registrar/templates/admin/model_descriptions.html +++ b/src/registrar/templates/admin/model_descriptions.html @@ -1,5 +1,5 @@ -
+
{% if opts.model_name == 'domainrequest' %}

This table contains all domain requests that have been started within the registrar and the status of those requests. @@ -184,5 +184,5 @@

This table does not have a description yet.

{% endif %}
- + \ No newline at end of file From 309b40392517e9c58293bbd1e9ea617c4719f009 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Apr 2024 13:45:47 -0600 Subject: [PATCH 08/76] Remove inline show more --- src/registrar/assets/js/get-gov-admin.js | 5 +- src/registrar/assets/sass/_theme/_admin.scss | 8 - .../templates/admin/model_descriptions.html | 345 +++++++++--------- 3 files changed, 173 insertions(+), 185 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 3a5c9c184..1f4581026 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -562,6 +562,7 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, let descriptionDiv = document.querySelector('.dja__model-description'); if (toggleButton && descriptionDiv) { + toggleButton.addEventListener('click', function() { // Toggle the class on the description div descriptionDiv.classList.toggle('dja__model-description--no-overflow'); @@ -569,12 +570,8 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, // Change the button text based on the presence of the class if (descriptionDiv.classList.contains('dja__model-description--no-overflow')) { toggleButton.textContent = 'Show less'; - - // Move the div to where it was when the page first loaded - descriptionDiv.appendChild(toggleButton); } else { toggleButton.textContent = 'Show more'; - descriptionDiv.insertAdjacentElement('afterend', toggleButton); } }); } diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 898d3d538..1813b0acc 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -636,15 +636,7 @@ div.dja__model-description{ } -.dja__model-description + button { - margin-top: 25px !important; - height: 10px !important; -} - div.dja__model-description.dja__model-description--no-overflow { display: block; overflow: unset; - button { - margin-top: unset; - } } diff --git a/src/registrar/templates/admin/model_descriptions.html b/src/registrar/templates/admin/model_descriptions.html index 425c0e677..68550e5bb 100644 --- a/src/registrar/templates/admin/model_descriptions.html +++ b/src/registrar/templates/admin/model_descriptions.html @@ -1,188 +1,187 @@ - -
- {% if opts.model_name == 'domainrequest' %} -

- This table contains all domain requests that have been started within the registrar and the status of those requests. - Updating values here will immediately update the corresponding values that users see in the registrar. -

-

- Once a domain request has been adjudicated, the details of that request should not be modified. - To update attributes (like an organization’s name) after a domain’s approval, - go to Domains. - Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request. -

- {% elif opts.model_name == 'domaininformation' %} -

- Domain information represents the basic metadata for an approved domain and the organization that manages it. - It does not include any information specific to the registry (DNS name servers, security email). - Registry-related information - can be managed within the Domains table. -

+
+ {% if opts.model_name == 'domainrequest' %} +

+ This table contains all domain requests that have been started within the registrar and the status of those requests. + Updating values here will immediately update the corresponding values that users see in the registrar. +

-

- Updating values here will immediately update the corresponding values that users see in the registrar. -

+

+ Once a domain request has been adjudicated, the details of that request should not be modified. + To update attributes (like an organization’s name) after a domain’s approval, + go to Domains. + Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request. +

+ {% elif opts.model_name == 'domaininformation' %} +

+ Domain information represents the basic metadata for an approved domain and the organization that manages it. + It does not include any information specific to the registry (DNS name servers, security email). + Registry-related information + can be managed within the Domains table. +

-

- Domain information is similar to Domain requests, - and the fields are nearly identical, - but edits made to one are not made to the other. - Domain information exists so we don’t modify details of an approved request after adjudication - (since a domain request should be maintained as-adjudicated for records retention purposes). - Entries are created here upon approval of a domain request. -

- {% elif opts.model_name == 'contact' %} -

- Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, - including other employees and authorizing officials. - Only contacts who have access to the registrar will have - a corresponding record within the Users table. - Updating someone’s contact information here will not affect that person’s Login.gov information. -

- {% elif opts.model_name == 'auditlog' %} -

- Log entries represent instances where something was created, updated, or deleted within the registrar or /admin. - The table on this page is useful for searching actions across all records. - To understand what happened to an individual record (like a domain request), - it’s better to go to that specific page - (Domain requests) and click on the “History” button. -

- {% elif opts.model_name == 'domain'%} -

- This table contains all approved domains in the .gov registrar. - In other words, with the exception of domains in the “Unknown”, ”On hold”, and “Deleted” states, - this table matches the list of domains in the .gov zone file. -

+

+ Updating values here will immediately update the corresponding values that users see in the registrar. +

-

- Individual domain pages allow you to view registry-related details and edit the associated domain information. -

+

+ Domain information is similar to Domain requests, + and the fields are nearly identical, + but edits made to one are not made to the other. + Domain information exists so we don’t modify details of an approved request after adjudication + (since a domain request should be maintained as-adjudicated for records retention purposes). + Entries are created here upon approval of a domain request. +

+ {% elif opts.model_name == 'contact' %} +

+ Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, + including other employees and authorizing officials. + Only contacts who have access to the registrar will have + a corresponding record within the Users table. + Updating someone’s contact information here will not affect that person’s Login.gov information. +

+ {% elif opts.model_name == 'auditlog' %} +

+ Log entries represent instances where something was created, updated, or deleted within the registrar or /admin. + The table on this page is useful for searching actions across all records. + To understand what happened to an individual record (like a domain request), + it’s better to go to that specific page + (Domain requests) and click on the “History” button. +

+ {% elif opts.model_name == 'domain'%} +

+ This table contains all approved domains in the .gov registrar. + In other words, with the exception of domains in the “Unknown”, ”On hold”, and “Deleted” states, + this table matches the list of domains in the .gov zone file. +

-

- Other actions available on the domain page include the ability to: -

-
    -
  • Extend a domain’s expiration date
  • -
  • Place a domain “On hold”
  • -
  • Remove a domain from the registry
  • -
  • View the domain from a domain manager’s perspective -
+

+ Individual domain pages allow you to view registry-related details and edit the associated domain information. +

-

- To view a domain from a domain manager’s perspective, click the "Manage domain" button. - That will allow you to make changes (e.g., update DNS settings, invite people to manage the domain) - directly inside the registrar. -

- {% elif opts.model_name == 'draftdomain' %} -

- This table represents all “requested domains” that have been saved within a domain request form. - If a registrant changes the requested domain in their form, - the original name they listed and the new name will appear as separate records. -

- -

- This table does not include “alternative domains,” - which are housed in the Websites table. -

- {% elif opts.model_name == 'host' %} -

- Entries in the Hosts table indicate the relationship between an approved domain and a name server address - (and, if applicable, the IP address for a name server address). -

+

+ Other actions available on the domain page include the ability to: +

+
    +
  • Extend a domain’s expiration date
  • +
  • Place a domain “On hold”
  • +
  • Remove a domain from the registry
  • +
  • View the domain from a domain manager’s perspective +
-

- In general, you should not modify these values here. They should be updated directly inside the registrar. - To update a domain’s name servers and/or IP addresses, - in the registrar, go to the domain in Domains, - then click the "Manage domain" button. -

- {% elif opts.model_name == 'publiccontact' %} -

- Public contacts represent the three registry contact types (administrative, technical, and security) - and their fields that are exposed in WHOIS data. -

+

+ To view a domain from a domain manager’s perspective, click the "Manage domain" button. + That will allow you to make changes (e.g., update DNS settings, invite people to manage the domain) + directly inside the registrar. +

+ {% elif opts.model_name == 'draftdomain' %} +

+ This table represents all “requested domains” that have been saved within a domain request form. + If a registrant changes the requested domain in their form, + the original name they listed and the new name will appear as separate records. +

+ +

+ This table does not include “alternative domains,” + which are housed in the Websites table. +

+ {% elif opts.model_name == 'host' %} +

+ Entries in the Hosts table indicate the relationship between an approved domain and a name server address + (and, if applicable, the IP address for a name server address). +

-

- We don’t currently allow registrants to publish real contact information to WHOIS, - but we must publish something to WHOIS. For each of the contact types, we use default values that are - associated with the program instead of the real contact information, - which we then redact in whole at the registry/WHOIS. - We do allow registrants to set a security contact email address, - which is published to WHOIS when a user sets one. -

+

+ In general, you should not modify these values here. They should be updated directly inside the registrar. + To update a domain’s name servers and/or IP addresses, + in the registrar, go to the domain in Domains, + then click the "Manage domain" button. +

+ {% elif opts.model_name == 'publiccontact' %} +

+ Public contacts represent the three registry contact types (administrative, technical, and security) + and their fields that are exposed in WHOIS data. +

-

- The public contacts in this table are a reflection of the data in the registry and should not be updated. - This information is primarily used by developers for validation purposes. -

- {% elif opts.model_name == 'transitiondomain' %} -

- This table represents the domains that were transitioned from the old registry in November 2023. - This data has been preserved for historical reference and should not be updated. -

- {% elif opts.model_name == 'userdomainrole' %} -

- This table represents the managers who are assigned to each domain in the registrar. - There are separate records for each domain/manager combination. - Managers can update information related to a domain, such as DNS data and security contact. -

+

+ We don’t currently allow registrants to publish real contact information to WHOIS, + but we must publish something to WHOIS. For each of the contact types, we use default values that are + associated with the program instead of the real contact information, + which we then redact in whole at the registry/WHOIS. + We do allow registrants to set a security contact email address, + which is published to WHOIS when a user sets one. +

-

- The creator of an approved domain request automatically becomes a manager for that domain. - Anyone who retrieves a domain invitation is also assigned the manager role. -

- {% elif opts.model_name == 'usergroup' %} -

- Groups are a way to bundle admin permissions so they can be easily assigned to multiple users. - Once a group is created, it can be assigned to people via the Users table. -

+

+ The public contacts in this table are a reflection of the data in the registry and should not be updated. + This information is primarily used by developers for validation purposes. +

+ {% elif opts.model_name == 'transitiondomain' %} +

+ This table represents the domains that were transitioned from the old registry in November 2023. + This data has been preserved for historical reference and should not be updated. +

+ {% elif opts.model_name == 'userdomainrole' %} +

+ This table represents the managers who are assigned to each domain in the registrar. + There are separate records for each domain/manager combination. + Managers can update information related to a domain, such as DNS data and security contact. +

-

- To maintain a single source of truth across all environments (stable, staging, etc.), - the groups and permissions are set and updated through the codebase. - Do not add, edit or delete user groups here. -

- {% elif opts.model_name == 'user' %} -

- A user is anyone who has access to the registrar. This includes request creators, domain managers, and CISA administrators. - Within each user record, you can review that user’s permissions and user status. -

+

+ The creator of an approved domain request automatically becomes a manager for that domain. + Anyone who retrieves a domain invitation is also assigned the manager role. +

+ {% elif opts.model_name == 'usergroup' %} +

+ Groups are a way to bundle admin permissions so they can be easily assigned to multiple users. + Once a group is created, it can be assigned to people via the Users table. +

-

- If a user has a domain request in ineligible status, then their user status will be “restricted.” - Users who are in restricted status cannot create/edit domain requests or approved domains, - and their existing requests are locked. They will see a 403 error if they try to take action in the registrar. -

+

+ To maintain a single source of truth across all environments (stable, staging, etc.), + the groups and permissions are set and updated through the codebase. + Do not add, edit or delete user groups here. +

+ {% elif opts.model_name == 'user' %} +

+ A user is anyone who has access to the registrar. This includes request creators, domain managers, and CISA administrators. + Within each user record, you can review that user’s permissions and user status. +

-

- Each user record displays the associated “contact” info for that user, - which is the same info found in the Contacts table. - Updating these details on the user record will also update the corresponding contact record for that user. -

- {% elif opts.model_name == 'verifiedbystaff' %} -

- This table contains users who have been allowed to bypass identity proofing through Login.gov after approval by a CISA representative. - Bypassing identity proofing means they will not be asked to provide a form of ID, PII, and so on. -

+

+ If a user has a domain request in ineligible status, then their user status will be “restricted.” + Users who are in restricted status cannot create/edit domain requests or approved domains, + and their existing requests are locked. They will see a 403 error if they try to take action in the registrar. +

-

- Additions to this table should be rare, and only after obtaining confirmation of their identity directly (or from a trusted person). - Once a verified-by-staff user has been added as a domain manager, they can be removed from this list, - (However, if they are removed as a domain manager for all domains and they attempt to sign in again, they will be identity proofed by Login.gov). -

- {% elif opts.model_name == 'website' %} -

- This table lists all the “current websites” and “alternative domains” that users have submitted in domain requests since January 2024. -

+

+ Each user record displays the associated “contact” info for that user, + which is the same info found in the Contacts table. + Updating these details on the user record will also update the corresponding contact record for that user. +

+ {% elif opts.model_name == 'verifiedbystaff' %} +

+ This table contains users who have been allowed to bypass identity proofing through Login.gov after approval by a CISA representative. + Bypassing identity proofing means they will not be asked to provide a form of ID, PII, and so on. +

-

- This does not include any “requested domains” that have appeared within the Domain requests table. - Those names are managed in the Draft domains table. -

- {% else %} -

This table does not have a description yet.

- {% endif %} -
- - \ No newline at end of file +

+ Additions to this table should be rare, and only after obtaining confirmation of their identity directly (or from a trusted person). + Once a verified-by-staff user has been added as a domain manager, they can be removed from this list, + (However, if they are removed as a domain manager for all domains and they attempt to sign in again, they will be identity proofed by Login.gov). +

+ {% elif opts.model_name == 'website' %} +

+ This table lists all the “current websites” and “alternative domains” that users have submitted in domain requests since January 2024. +

+ +

+ This does not include any “requested domains” that have appeared within the Domain requests table. + Those names are managed in the Draft domains table. +

+ {% else %} +

This table does not have a description yet.

+ {% endif %} +
+ From edd74809215ccf74952276cf6c3f36e97e9521f9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Apr 2024 08:41:33 -0600 Subject: [PATCH 09/76] Update _admin.scss --- src/registrar/assets/sass/_theme/_admin.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 1813b0acc..99bf2017a 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -634,9 +634,9 @@ div.dja__model-description{ list-style-type: disc; } -} + &.dja__model-description--no-overflow { + display: block; + overflow: unset; + } -div.dja__model-description.dja__model-description--no-overflow { - display: block; - overflow: unset; } From 8ed59d9bc8693511d981d094c00f14d520531061 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Apr 2024 08:43:25 -0600 Subject: [PATCH 10/76] Update admin.py --- src/registrar/admin.py | 43 ------------------------------------------ 1 file changed, 43 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index e2e3fb35b..42d73f10d 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -382,49 +382,6 @@ class AdminSortFields: class AuditedAdmin(admin.ModelAdmin): """Custom admin to make auditing easier.""" - # TODO - move this into a helper somewhere, maybe an enum? - model_descriptions = { - 'domainrequest': ( - "This table contains all domain requests that have been started within the registrar and the " - "status of those requests. " - "Updating values here will immediately update the corresponding values that users see in the registrar. " - "Once a domain request has been adjudicated, the details of that request should not be modified. " - "To update attributes (like an organization’s name) after a domain’s approval, go to Domains. " - "Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request." - ), - "contact": ( - "Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, " - "including other employees and authorizing officials. " - "Only contacts who have access to the registrar will have a corresponding record within the " - "Users table." - # TODO - change this - "

" - "Updating someone’s contact information here will not affect that person’s Login.gov information." - ), - "domaininformation": ( - "Domain information represents the basic metadata for an approved domain and " - "the organization that manages it. " - "It does not include any information specific to the registry (DNS name servers, security email). " - "Registry-related information can be managed within the Domains table. " - "Updating values here will immediately update the corresponding values that users see in the registrar." - # TODO - change this - "

" - "Domain information is similar to Domain requests, and the fields are nearly identical," - "but edits made to one are not made to the other." - "Domain information exists so we don’t modify details of an approved request after " - "adjudication (since a domain request should be maintained as-adjudicated for records retention purposes). " - "Entries are created here upon approval of a domain request. " - ) - } - - def changelist_view(self, request, extra_context=None): - extra_context = extra_context or {} - model_name = self.model._meta.model_name - if model_name in self.model_descriptions: - extra_context['model_description'] = self.model_descriptions[model_name] - - return super().changelist_view(request, extra_context=extra_context) - def history_view(self, request, object_id, extra_context=None): """On clicking 'History', take admin to the auditlog view for an object.""" return HttpResponseRedirect( From 1ca79550159911146d2885cc1f5fda5685d7d58b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Apr 2024 09:53:43 -0600 Subject: [PATCH 11/76] Refactor --- src/registrar/assets/js/get-gov-admin.js | 40 +++++++++++++------ src/registrar/assets/sass/_theme/_admin.scss | 13 +++++- .../templates/admin/model_descriptions.html | 4 +- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 1f4581026..307bce05b 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -558,21 +558,35 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, /** An IIFE for toggling the overflow styles on django-admin__model-description (the show more / show less button) */ (function () { + function handleShowMoreButton(toggleButton, descriptionDiv){ + // Check the length of the text content in the description div + if (descriptionDiv.textContent.length < 200) { + // Hide the toggle button if text content is less than 200 characters + // This is a little over 160 characters to give us some wiggle room if we + // change the font size marginally. + toggleButton.classList.add('display-none'); + } else { + toggleButton.addEventListener('click', function() { + toggleShowMoreButton(toggleButton, descriptionDiv, 'dja__model-description--no-overflow') + }); + } + } + + function toggleShowMoreButton(toggleButton, descriptionDiv, showMoreClassName){ + // Toggle the class on the description div + descriptionDiv.classList.toggle(showMoreClassName); + + // Change the button text based on the presence of the class + if (descriptionDiv.classList.contains(showMoreClassName)) { + toggleButton.textContent = 'Show less'; + } else { + toggleButton.textContent = 'Show more'; + } + } + let toggleButton = document.getElementById('dja-show-more-model-description'); let descriptionDiv = document.querySelector('.dja__model-description'); - if (toggleButton && descriptionDiv) { - - toggleButton.addEventListener('click', function() { - // Toggle the class on the description div - descriptionDiv.classList.toggle('dja__model-description--no-overflow'); - - // Change the button text based on the presence of the class - if (descriptionDiv.classList.contains('dja__model-description--no-overflow')) { - toggleButton.textContent = 'Show less'; - } else { - toggleButton.textContent = 'Show more'; - } - }); + handleShowMoreButton(toggleButton, descriptionDiv) } })(); \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 99bf2017a..b5bf735ca 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -625,11 +625,22 @@ div.dja__model-description{ overflow: hidden; p, li { - color: var(--primary); font-size: medium; line-height: 1.2em; } + @media (prefers-color-scheme: light) { + p, li { + color: var(--body-fg); + } + } + + @media (prefers-color-scheme: dark) { + p, li { + color: var(--primary); + } + } + li { list-style-type: disc; } diff --git a/src/registrar/templates/admin/model_descriptions.html b/src/registrar/templates/admin/model_descriptions.html index 68550e5bb..7120e5e6b 100644 --- a/src/registrar/templates/admin/model_descriptions.html +++ b/src/registrar/templates/admin/model_descriptions.html @@ -9,7 +9,7 @@

Once a domain request has been adjudicated, the details of that request should not be modified. To update attributes (like an organization’s name) after a domain’s approval, - go to Domains. + go to Domains. Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request.

{% elif opts.model_name == 'domaininformation' %} @@ -140,7 +140,7 @@

To maintain a single source of truth across all environments (stable, staging, etc.), - the groups and permissions are set and updated through the codebase. + the groups and permissions are set and updated through the codebase. Do not add, edit or delete user groups here.

{% elif opts.model_name == 'user' %} From ac923701a02e90d76d7caebc4133beb6a45626a4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:14:41 -0600 Subject: [PATCH 12/76] Unit tests part 1 --- src/registrar/tests/test_admin.py | 176 +++++++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 3 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 42baae6ef..d6beb7456 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -95,6 +95,23 @@ class TestDomainAdmin(MockEppLib, WebTest): ) super().setUp() + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/domain/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains(response, "This table contains all approved domains in the .gov registrar.") + self.assertContains(response, "Show more") + @less_console_noise_decorator def test_contact_fields_on_domain_change_form_have_detail_table(self): """Tests if the contact fields in the inlined Domain information have the detail table @@ -852,6 +869,23 @@ class TestDomainRequestAdmin(MockEppLib): ) self.mock_client = MockSESClient() + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/domainrequest/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains(response, "This table contains all domain requests") + self.assertContains(response, "Show more") + @less_console_noise_decorator def test_helper_text(self): """ @@ -2377,7 +2411,7 @@ class TestDomainRequestAdmin(MockEppLib): self.mock_client.EMAILS_SENT.clear() -class DomainInvitationAdminTest(TestCase): +class TestDomainInvitationAdmin(TestCase): """Tests for the DomainInvitation page""" def setUp(self): @@ -2393,6 +2427,26 @@ class DomainInvitationAdminTest(TestCase): User.objects.all().delete() Contact.objects.all().delete() + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/domaininvitation/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains( + response, + "Domain invitations contain all individuals who have been invited to manage a .gov domain." + ) + self.assertContains(response, "Show more") + def test_get_filters(self): """Ensures that our filters are displaying correctly""" with less_console_noise(): @@ -2442,6 +2496,26 @@ class TestHostAdmin(TestCase): Host.objects.all().delete() Domain.objects.all().delete() + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/host/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains( + response, + "Entries in the Hosts table indicate the relationship between an approved domain" + ) + self.assertContains(response, "Show more") + @less_console_noise_decorator def test_helper_text(self): """ @@ -2520,6 +2594,23 @@ class TestDomainInformationAdmin(TestCase): Contact.objects.all().delete() User.objects.all().delete() + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/domaininformation/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains(response, "Domain information represents the basic metadata") + self.assertContains(response, "Show more") + @less_console_noise_decorator def test_helper_text(self): """ @@ -2763,7 +2854,7 @@ class TestDomainInformationAdmin(TestCase): self.test_helper.assert_table_sorted("-4", ("-submitter__first_name", "-submitter__last_name")) -class UserDomainRoleAdminTest(TestCase): +class TestUserDomainRoleAdmin(TestCase): def setUp(self): """Setup environment for a mock admin user""" self.site = AdminSite() @@ -2785,6 +2876,23 @@ class UserDomainRoleAdminTest(TestCase): Domain.objects.all().delete() UserDomainRole.objects.all().delete() + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/userdomainrole/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains(response, "This table represents the managers who are assigned to each domain in the registrar") + self.assertContains(response, "Show more") + def test_domain_sortable(self): """Tests if the UserDomainrole sorts by domain correctly""" with less_console_noise(): @@ -2981,6 +3089,27 @@ class TestMyUserAdmin(TestCase): super().tearDown() User.objects.all().delete() + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/user/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains( + response, + "A user is anyone who has access to the registrar." + ) + self.assertContains(response, "Show more") + + @less_console_noise_decorator def test_helper_text(self): """ @@ -3407,7 +3536,7 @@ class DomainSessionVariableTest(TestCase): ) -class ContactAdminTest(TestCase): +class TestContactAdmin(TestCase): def setUp(self): self.site = AdminSite() self.factory = RequestFactory() @@ -3416,6 +3545,26 @@ class ContactAdminTest(TestCase): self.superuser = create_superuser() self.staffuser = create_user() + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/contact/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains( + response, + "Contacts include anyone who has access to the registrar (known as “users”)" + ) + self.assertContains(response, "Show more") + def test_readonly_when_restricted_staffuser(self): with less_console_noise(): request = self.factory.get("/") @@ -3534,6 +3683,27 @@ class TestVerifiedByStaffAdmin(TestCase): VerifiedByStaff.objects.all().delete() User.objects.all().delete() + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/verifiedbystaff/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains( + response, + "This table contains users who have been allowed to bypass " + "identity proofing through Login.gov" + ) + self.assertContains(response, "Show more") + @less_console_noise_decorator def test_helper_text(self): """ From 9a2ee18cb9de206fdd8169e075019f3be89332b9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:17:54 -0600 Subject: [PATCH 13/76] Add domain invitation content --- .../templates/admin/model_descriptions.html | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/registrar/templates/admin/model_descriptions.html b/src/registrar/templates/admin/model_descriptions.html index 7120e5e6b..afe3f5131 100644 --- a/src/registrar/templates/admin/model_descriptions.html +++ b/src/registrar/templates/admin/model_descriptions.html @@ -32,6 +32,23 @@ (since a domain request should be maintained as-adjudicated for records retention purposes). Entries are created here upon approval of a domain request.

+ {% elif opts.model_name == 'domaininvitation' %} +

+ Domain invitations contain all individuals who have been invited to manage a .gov domain. + Invitations are sent via email, and the recipient must log in to the registrar to officially + accept and become a domain manager. +

+ +

+ An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. + A “received” status indicates that the recipient has logged in. +

+ +

+ If an invitation is created in this table, an email will not be sent. + To have an email sent, go to the domain in Domains, + click the “Manage domain” button, and add a domain manager. +

{% elif opts.model_name == 'contact' %}

Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, From b89db7ff27690623c1040ee822ef81e5b80300dc Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:34:26 -0600 Subject: [PATCH 14/76] Add missing unit tests --- .../templates/admin/model_descriptions.html | 28 +-- src/registrar/tests/test_admin.py | 238 ++++++++++++++++-- 2 files changed, 233 insertions(+), 33 deletions(-) diff --git a/src/registrar/templates/admin/model_descriptions.html b/src/registrar/templates/admin/model_descriptions.html index afe3f5131..f87bf61ca 100644 --- a/src/registrar/templates/admin/model_descriptions.html +++ b/src/registrar/templates/admin/model_descriptions.html @@ -33,22 +33,22 @@ Entries are created here upon approval of a domain request.

{% elif opts.model_name == 'domaininvitation' %} -

- Domain invitations contain all individuals who have been invited to manage a .gov domain. - Invitations are sent via email, and the recipient must log in to the registrar to officially - accept and become a domain manager. -

+

+ Domain invitations contain all individuals who have been invited to manage a .gov domain. + Invitations are sent via email, and the recipient must log in to the registrar to officially + accept and become a domain manager. +

-

- An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. - A “received” status indicates that the recipient has logged in. -

+

+ An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. + A “received” status indicates that the recipient has logged in. +

-

- If an invitation is created in this table, an email will not be sent. - To have an email sent, go to the domain in Domains, - click the “Manage domain” button, and add a domain manager. -

+

+ If an invitation is created in this table, an email will not be sent. + To have an email sent, go to the domain in Domains, + click the “Manage domain” button, and add a domain manager. +

{% elif opts.model_name == 'contact' %}

Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index d6beb7456..6065f85b9 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -21,6 +21,13 @@ from registrar.admin import ( MyHostAdmin, UserDomainRoleAdmin, VerifiedByStaffAdmin, + WebsiteAdmin, + DraftDomainAdmin, + FederalAgencyAdmin, + PublicContactAdmin, + TransitionDomainAdmin, + UserGroupAdmin, + TransitionDomainAdmin, ) from registrar.models import ( Domain, @@ -33,6 +40,9 @@ from registrar.models import ( PublicContact, Host, Website, + FederalAgency, + UserGroup, + TransitionDomain, ) from registrar.models.user_domain_role import UserDomainRole from registrar.models.verified_by_staff import VerifiedByStaff @@ -2442,8 +2452,7 @@ class TestDomainInvitationAdmin(TestCase): # Test for a description snippet self.assertContains( - response, - "Domain invitations contain all individuals who have been invited to manage a .gov domain." + response, "Domain invitations contain all individuals who have been invited to manage a .gov domain." ) self.assertContains(response, "Show more") @@ -2510,10 +2519,7 @@ class TestHostAdmin(TestCase): self.assertEqual(response.status_code, 200) # Test for a description snippet - self.assertContains( - response, - "Entries in the Hosts table indicate the relationship between an approved domain" - ) + self.assertContains(response, "Entries in the Hosts table indicate the relationship between an approved domain") self.assertContains(response, "Show more") @less_console_noise_decorator @@ -2890,7 +2896,9 @@ class TestUserDomainRoleAdmin(TestCase): self.assertEqual(response.status_code, 200) # Test for a description snippet - self.assertContains(response, "This table represents the managers who are assigned to each domain in the registrar") + self.assertContains( + response, "This table represents the managers who are assigned to each domain in the registrar" + ) self.assertContains(response, "Show more") def test_domain_sortable(self): @@ -3103,13 +3111,9 @@ class TestMyUserAdmin(TestCase): self.assertEqual(response.status_code, 200) # Test for a description snippet - self.assertContains( - response, - "A user is anyone who has access to the registrar." - ) + self.assertContains(response, "A user is anyone who has access to the registrar.") self.assertContains(response, "Show more") - @less_console_noise_decorator def test_helper_text(self): """ @@ -3559,10 +3563,7 @@ class TestContactAdmin(TestCase): self.assertEqual(response.status_code, 200) # Test for a description snippet - self.assertContains( - response, - "Contacts include anyone who has access to the registrar (known as “users”)" - ) + self.assertContains(response, "Contacts include anyone who has access to the registrar (known as “users”)") self.assertContains(response, "Show more") def test_readonly_when_restricted_staffuser(self): @@ -3698,9 +3699,7 @@ class TestVerifiedByStaffAdmin(TestCase): # Test for a description snippet self.assertContains( - response, - "This table contains users who have been allowed to bypass " - "identity proofing through Login.gov" + response, "This table contains users who have been allowed to bypass " "identity proofing through Login.gov" ) self.assertContains(response, "Show more") @@ -3746,3 +3745,204 @@ class TestVerifiedByStaffAdmin(TestCase): # Check that the user field is set to the request.user self.assertEqual(vip_instance.requestor, self.superuser) + + +class TestWebsiteAdmin(TestCase): + def setUp(self): + super().setUp() + self.site = AdminSite() + self.superuser = create_superuser() + self.admin = WebsiteAdmin(model=Website, admin_site=self.site) + self.factory = RequestFactory() + self.client = Client(HTTP_HOST="localhost:8080") + self.test_helper = GenericTestHelper(admin=self.admin) + + def tearDown(self): + super().tearDown() + Website.objects.all().delete() + User.objects.all().delete() + + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/website/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains(response, "This table lists all the “current websites” and “alternative domains”") + self.assertContains(response, "Show more") + + +class TestDraftDomain(TestCase): + def setUp(self): + super().setUp() + self.site = AdminSite() + self.superuser = create_superuser() + self.admin = DraftDomainAdmin(model=DraftDomain, admin_site=self.site) + self.factory = RequestFactory() + self.client = Client(HTTP_HOST="localhost:8080") + self.test_helper = GenericTestHelper(admin=self.admin) + + def tearDown(self): + super().tearDown() + DraftDomain.objects.all().delete() + User.objects.all().delete() + + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/draftdomain/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains( + response, "This table represents all “requested domains” that have been saved within a domain" + ) + self.assertContains(response, "Show more") + + +class TestFederalAgency(TestCase): + def setUp(self): + super().setUp() + self.site = AdminSite() + self.superuser = create_superuser() + self.admin = FederalAgencyAdmin(model=FederalAgency, admin_site=self.site) + self.factory = RequestFactory() + self.client = Client(HTTP_HOST="localhost:8080") + self.test_helper = GenericTestHelper(admin=self.admin) + + def tearDown(self): + super().tearDown() + FederalAgency.objects.all().delete() + User.objects.all().delete() + + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/federalagency/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains(response, "This table does not have a description yet.") + self.assertNotContains(response, "Show more") + + +class TestPublicContact(TestCase): + def setUp(self): + super().setUp() + self.site = AdminSite() + self.superuser = create_superuser() + self.admin = PublicContactAdmin(model=PublicContact, admin_site=self.site) + self.factory = RequestFactory() + self.client = Client(HTTP_HOST="localhost:8080") + self.test_helper = GenericTestHelper(admin=self.admin) + + def tearDown(self): + super().tearDown() + PublicContact.objects.all().delete() + User.objects.all().delete() + + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/publiccontact/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains(response, "Public contacts represent the three registry contact types") + self.assertContains(response, "Show more") + + +class TestTransitionDomain(TestCase): + def setUp(self): + super().setUp() + self.site = AdminSite() + self.superuser = create_superuser() + self.admin = TransitionDomainAdmin(model=TransitionDomain, admin_site=self.site) + self.factory = RequestFactory() + self.client = Client(HTTP_HOST="localhost:8080") + self.test_helper = GenericTestHelper(admin=self.admin) + + def tearDown(self): + super().tearDown() + PublicContact.objects.all().delete() + User.objects.all().delete() + + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/transitiondomain/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains(response, "This table represents the domains that were transitioned from the old registry") + self.assertContains(response, "Show more") + + +class TestUserGroup(TestCase): + def setUp(self): + super().setUp() + self.site = AdminSite() + self.superuser = create_superuser() + self.admin = UserGroupAdmin(model=UserGroup, admin_site=self.site) + self.factory = RequestFactory() + self.client = Client(HTTP_HOST="localhost:8080") + self.test_helper = GenericTestHelper(admin=self.admin) + + def tearDown(self): + super().tearDown() + User.objects.all().delete() + + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/usergroup/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains( + response, "Groups are a way to bundle admin permissions so they can be easily assigned to multiple users." + ) + self.assertContains(response, "Show more") From d760f2dab4b64e7217c3f623fb9c09382cbdd684 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:45:13 -0600 Subject: [PATCH 15/76] Fix unit tests --- src/registrar/tests/test_admin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 6065f85b9..88f00bfcb 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -538,7 +538,7 @@ class TestDomainAdmin(MockEppLib, WebTest): # There are 4 template references to Federal (4) plus four references in the table # for our actual domain_request - self.assertContains(response, "Federal", count=36) + self.assertContains(response, "Federal", count=39) # This may be a bit more robust self.assertContains(response, 'Federal', count=1) # Now let's make sure the long description does not exist @@ -1200,7 +1200,7 @@ class TestDomainRequestAdmin(MockEppLib): response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal") # There are 2 template references to Federal (4) and two in the results data # of the request - self.assertContains(response, "Federal", count=34) + self.assertContains(response, "Federal", count=37) # This may be a bit more robust self.assertContains(response, 'Federal', count=1) # Now let's make sure the long description does not exist @@ -2470,7 +2470,7 @@ class TestDomainInvitationAdmin(TestCase): ) # Assert that the filters are added - self.assertContains(response, "invited", count=2) + self.assertContains(response, "invited", count=4) self.assertContains(response, "Invited", count=2) self.assertContains(response, "retrieved", count=2) self.assertContains(response, "Retrieved", count=2) @@ -3845,7 +3845,7 @@ class TestFederalAgency(TestCase): # Test for a description snippet self.assertContains(response, "This table does not have a description yet.") - self.assertNotContains(response, "Show more") + self.assertContains(response, "Show more") class TestPublicContact(TestCase): From e1755578c4ca2163c86f91c8085688526a99195d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:57:22 -0600 Subject: [PATCH 16/76] Linting --- src/registrar/tests/test_admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 88f00bfcb..49848fd3a 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -27,7 +27,6 @@ from registrar.admin import ( PublicContactAdmin, TransitionDomainAdmin, UserGroupAdmin, - TransitionDomainAdmin, ) from registrar.models import ( Domain, From 1ea4b0b6f52f7a403aab958bd91d314706775e55 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:54:54 -0600 Subject: [PATCH 17/76] auditlog -=> log entry --- src/registrar/templates/admin/model_descriptions.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/admin/model_descriptions.html b/src/registrar/templates/admin/model_descriptions.html index f87bf61ca..5599baf18 100644 --- a/src/registrar/templates/admin/model_descriptions.html +++ b/src/registrar/templates/admin/model_descriptions.html @@ -57,7 +57,7 @@ a corresponding record within the Users table. Updating someone’s contact information here will not affect that person’s Login.gov information.

- {% elif opts.model_name == 'auditlog' %} + {% elif opts.model_name == 'logentry' %}

Log entries represent instances where something was created, updated, or deleted within the registrar or /admin. The table on this page is useful for searching actions across all records. From 9daa6ddafefa28b005f7bc4ff11239eeb048a92d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Apr 2024 12:06:20 -0600 Subject: [PATCH 18/76] Change style --- .../templates/admin/model_descriptions.html | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/registrar/templates/admin/model_descriptions.html b/src/registrar/templates/admin/model_descriptions.html index 5599baf18..6469f6188 100644 --- a/src/registrar/templates/admin/model_descriptions.html +++ b/src/registrar/templates/admin/model_descriptions.html @@ -9,7 +9,7 @@

Once a domain request has been adjudicated, the details of that request should not be modified. To update attributes (like an organization’s name) after a domain’s approval, - go to Domains. + go to Domains. Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request.

{% elif opts.model_name == 'domaininformation' %} @@ -17,7 +17,7 @@ Domain information represents the basic metadata for an approved domain and the organization that manages it. It does not include any information specific to the registry (DNS name servers, security email). Registry-related information - can be managed within the Domains table. + can be managed within the Domains table.

@@ -25,7 +25,7 @@

- Domain information is similar to Domain requests, + Domain information is similar to Domain requests, and the fields are nearly identical, but edits made to one are not made to the other. Domain information exists so we don’t modify details of an approved request after adjudication @@ -46,7 +46,7 @@

If an invitation is created in this table, an email will not be sent. - To have an email sent, go to the domain in Domains, + To have an email sent, go to the domain in Domains, click the “Manage domain” button, and add a domain manager.

{% elif opts.model_name == 'contact' %} @@ -54,7 +54,7 @@ Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, including other employees and authorizing officials. Only contacts who have access to the registrar will have - a corresponding record within the Users table. + a corresponding record within the Users table. Updating someone’s contact information here will not affect that person’s Login.gov information.

{% elif opts.model_name == 'logentry' %} @@ -63,7 +63,7 @@ The table on this page is useful for searching actions across all records. To understand what happened to an individual record (like a domain request), it’s better to go to that specific page - (Domain requests) and click on the “History” button. + (Domain requests) and click on the “History” button.

{% elif opts.model_name == 'domain'%}

@@ -100,7 +100,7 @@

This table does not include “alternative domains,” - which are housed in the Websites table. + which are housed in the Websites table.

{% elif opts.model_name == 'host' %}

@@ -111,7 +111,7 @@

In general, you should not modify these values here. They should be updated directly inside the registrar. To update a domain’s name servers and/or IP addresses, - in the registrar, go to the domain in Domains, + in the registrar, go to the domain in Domains, then click the "Manage domain" button.

{% elif opts.model_name == 'publiccontact' %} @@ -152,7 +152,7 @@ {% elif opts.model_name == 'usergroup' %}

Groups are a way to bundle admin permissions so they can be easily assigned to multiple users. - Once a group is created, it can be assigned to people via the Users table. + Once a group is created, it can be assigned to people via the Users table.

@@ -174,7 +174,7 @@

Each user record displays the associated “contact” info for that user, - which is the same info found in the Contacts table. + which is the same info found in the Contacts table. Updating these details on the user record will also update the corresponding contact record for that user.

{% elif opts.model_name == 'verifiedbystaff' %} @@ -194,8 +194,8 @@

- This does not include any “requested domains” that have appeared within the Domain requests table. - Those names are managed in the Draft domains table. + This does not include any “requested domains” that have appeared within the Domain requests table. + Those names are managed in the Draft domains table.

{% else %}

This table does not have a description yet.

From 781b9f7b810b9003e745b96ce9f608937bf43aa8 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 29 Apr 2024 18:39:00 -0400 Subject: [PATCH 19/76] if phone number is already filled in on user, do not overwrite --- src/djangooidc/backends.py | 30 +++++++++++++-- src/djangooidc/tests/test_backends.py | 53 +++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index b0e6417db..69b34d775 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -65,13 +65,35 @@ class OpenIdConnectBackend(ModelBackend): return user def update_existing_user(self, user, kwargs): - """Update other fields without overwriting first_name and last_name. - Overwrite first_name and last_name if not empty string""" + """ + Update user fields without overwriting certain fields. + Args: + user: User object to be updated. + kwargs: Dictionary containing fields to update and their new values. + + Note: + This method updates user fields while preserving the values of 'first_name', + 'last_name', and 'phone' fields, unless specific conditions are met. + + - 'phone' field will be updated if it's None or an empty string. + - 'first_name' and 'last_name' will be updated if the provided value is not empty. + """ + + # Iterate over fields to update for key, value in kwargs.items(): - # Check if the key is not first_name or last_name or value is not empty string - if key not in ["first_name", "last_name"] or value: + # Check if the field is not 'first_name', 'last_name', or 'phone', + # or if it's the 'phone' field and 'user.phone' is None or empty, + # or if it's 'first_name' or 'last_name' and the provided value is not empty + if ( + key not in ["first_name", "last_name", "phone"] + or (key == "phone" and (user.phone is None or user.phone == "")) + or (key in ["first_name", "last_name"] and value) + ): + # Update the corresponding attribute of the user object setattr(user, key, value) + + # Save the user object with the updated fields user.save() def clean_username(self, username): diff --git a/src/djangooidc/tests/test_backends.py b/src/djangooidc/tests/test_backends.py index ac7f74903..ffe26e2cb 100644 --- a/src/djangooidc/tests/test_backends.py +++ b/src/djangooidc/tests/test_backends.py @@ -50,11 +50,16 @@ class OpenIdConnectBackendTestCase(TestCase): self.assertEqual(user.email, "john.doe@example.com") self.assertEqual(user.phone, "123456789") - def test_authenticate_with_existing_user_no_name(self): + def test_authenticate_with_existing_user_with_existing_first_last_phone(self): """Test that authenticate updates an existing user if it finds one. - For this test, given_name and family_name are not supplied""" + For this test, given_name and family_name are not supplied. + + The existing user's first and last name are not overwritten. + The existing user's phone number is not overwritten""" # Create an existing user with the same username and with first and last names - existing_user = User.objects.create_user(username="test_user", first_name="John", last_name="Doe") + existing_user = User.objects.create_user( + username="test_user", first_name="WillNotBe", last_name="Replaced", phone="9999999999" + ) # Remove given_name and family_name from the input, self.kwargs self.kwargs.pop("given_name", None) @@ -67,6 +72,48 @@ class OpenIdConnectBackendTestCase(TestCase): self.assertIsInstance(user, User) self.assertEqual(user, existing_user) # The same user instance should be returned + # Verify that user fields are correctly updated + self.assertEqual(user.first_name, "WillNotBe") + self.assertEqual(user.last_name, "Replaced") + self.assertEqual(user.email, "john.doe@example.com") + self.assertEqual(user.phone, "9999999999") + + def test_authenticate_with_existing_user_with_no_phone_will_update_phone(self): + """Test that authenticate updates an existing user if it finds one. + For this test, the existing user has an empty string for phone + + The existing user's phone number is overwritten""" + # Create an existing user with the same username and with first and last names + existing_user = User.objects.create_user(username="test_user", phone="") + + # Ensure that the authenticate method updates the existing user + # and preserves existing first and last names + user = self.backend.authenticate(request=None, **self.kwargs) + self.assertIsNotNone(user) + self.assertIsInstance(user, User) + self.assertEqual(user, existing_user) # The same user instance should be returned + + # Verify that user fields are correctly updated + self.assertEqual(user.first_name, "John") + self.assertEqual(user.last_name, "Doe") + self.assertEqual(user.email, "john.doe@example.com") + self.assertEqual(user.phone, "123456789") + + def test_authenticate_with_existing_user_with_none_phone_will_update_phone(self): + """Test that authenticate updates an existing user if it finds one. + For this test, the existing user has None for phone + + The existing user's phone number is overwritten""" + # Create an existing user with the same username and with first and last names + existing_user = User.objects.create_user(username="test_user") + + # Ensure that the authenticate method updates the existing user + # and preserves existing first and last names + user = self.backend.authenticate(request=None, **self.kwargs) + self.assertIsNotNone(user) + self.assertIsInstance(user, User) + self.assertEqual(user, existing_user) # The same user instance should be returned + # Verify that user fields are correctly updated self.assertEqual(user.first_name, "John") self.assertEqual(user.last_name, "Doe") From 8b36fef5c1dad003991d37f7302e33e570e7e71f Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 29 Apr 2024 19:41:51 -0400 Subject: [PATCH 20/76] If phone number is already filled in for the associated contact, do not overwrite --- src/djangooidc/backends.py | 12 +++++++++++- src/registrar/models/contact.py | 3 ++- src/registrar/tests/common.py | 2 +- src/registrar/tests/test_models.py | 24 ++++++++++++++++++++---- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 69b34d775..6556d8d7a 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -8,6 +8,8 @@ from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend from django.utils import timezone +from registrar.models.contact import Contact + logger = logging.getLogger(__name__) @@ -80,6 +82,14 @@ class OpenIdConnectBackend(ModelBackend): - 'first_name' and 'last_name' will be updated if the provided value is not empty. """ + contacts = Contact.objects.filter(user=user) + + if len(contacts) == 0: # no matching contact + logger.warning("Could not find a contact when one should've existed.") + + if len(contacts) > 1: # multiple matches + logger.warning("There are multiple Contacts with the same email address.") + # Iterate over fields to update for key, value in kwargs.items(): # Check if the field is not 'first_name', 'last_name', or 'phone', @@ -87,7 +97,7 @@ class OpenIdConnectBackend(ModelBackend): # or if it's 'first_name' or 'last_name' and the provided value is not empty if ( key not in ["first_name", "last_name", "phone"] - or (key == "phone" and (user.phone is None or user.phone == "")) + or (key == "phone" and not contacts[0].phone) or (key in ["first_name", "last_name"] and value) ): # Update the corresponding attribute of the user object diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 9deb22641..185b84ded 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -102,9 +102,10 @@ class Contact(TimeStampedModel): super().save(*args, **kwargs) # Update the related User object's first_name and last_name - if self.user and (not self.user.first_name or not self.user.last_name): + if self.user and (not self.user.first_name or not self.user.last_name or not self.user.phone): self.user.first_name = self.first_name self.user.last_name = self.last_name + self.user.phone = self.phone self.user.save() def __str__(self): diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 6dd88c1c1..b895e2916 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -834,7 +834,7 @@ def completed_domain_request( ) if not investigator: investigator, _ = User.objects.get_or_create( - username="incrediblyfakeinvestigator", + username="incrediblyfakeinvestigator" + str(uuid.uuid4())[:8], first_name="Joe", last_name="Bob", is_staff=True, diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index ca77d1ddf..1558ab310 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1152,12 +1152,18 @@ class TestContact(TestCase): def setUp(self): self.email_for_invalid = "intern@igorville.gov" self.invalid_user, _ = User.objects.get_or_create( - username=self.email_for_invalid, email=self.email_for_invalid, first_name="", last_name="" + username=self.email_for_invalid, + email=self.email_for_invalid, + first_name="", + last_name="", + phone="", ) self.invalid_contact, _ = Contact.objects.get_or_create(user=self.invalid_user) self.email = "mayor@igorville.gov" - self.user, _ = User.objects.get_or_create(email=self.email, first_name="Jeff", last_name="Lebowski") + self.user, _ = User.objects.get_or_create( + email=self.email, first_name="Jeff", last_name="Lebowski", phone="123456789" + ) self.contact, _ = Contact.objects.get_or_create(user=self.user) self.contact_as_ao, _ = Contact.objects.get_or_create(email="newguy@igorville.gov") @@ -1169,19 +1175,22 @@ class TestContact(TestCase): Contact.objects.all().delete() User.objects.all().delete() - def test_saving_contact_updates_user_first_last_names(self): + def test_saving_contact_updates_user_first_last_names_and_phone(self): """When a contact is updated, we propagate the changes to the linked user if it exists.""" # User and Contact are created and linked as expected. # An empty User object should create an empty contact. self.assertEqual(self.invalid_contact.first_name, "") self.assertEqual(self.invalid_contact.last_name, "") + self.assertEqual(self.invalid_contact.phone, "") self.assertEqual(self.invalid_user.first_name, "") self.assertEqual(self.invalid_user.last_name, "") + self.assertEqual(self.invalid_user.phone, "") # Manually update the contact - mimicking production (pre-existing data) self.invalid_contact.first_name = "Joey" self.invalid_contact.last_name = "Baloney" + self.invalid_contact.phone = "123456789" self.invalid_contact.save() # Refresh the user object to reflect the changes made in the database @@ -1190,20 +1199,25 @@ class TestContact(TestCase): # Updating the contact's first and last names propagate to the user self.assertEqual(self.invalid_contact.first_name, "Joey") self.assertEqual(self.invalid_contact.last_name, "Baloney") + self.assertEqual(self.invalid_contact.phone, "123456789") self.assertEqual(self.invalid_user.first_name, "Joey") self.assertEqual(self.invalid_user.last_name, "Baloney") + self.assertEqual(self.invalid_user.phone, "123456789") - def test_saving_contact_does_not_update_user_first_last_names(self): + def test_saving_contact_does_not_update_user_first_last_names_and_phone(self): """When a contact is updated, we avoid propagating the changes to the linked user if it already has a value""" # User and Contact are created and linked as expected self.assertEqual(self.contact.first_name, "Jeff") self.assertEqual(self.contact.last_name, "Lebowski") + self.assertEqual(self.contact.phone, "123456789") self.assertEqual(self.user.first_name, "Jeff") self.assertEqual(self.user.last_name, "Lebowski") + self.assertEqual(self.user.phone, "123456789") self.contact.first_name = "Joey" self.contact.last_name = "Baloney" + self.contact.phone = "987654321" self.contact.save() # Refresh the user object to reflect the changes made in the database @@ -1212,8 +1226,10 @@ class TestContact(TestCase): # Updating the contact's first and last names propagate to the user self.assertEqual(self.contact.first_name, "Joey") self.assertEqual(self.contact.last_name, "Baloney") + self.assertEqual(self.contact.phone, "987654321") self.assertEqual(self.user.first_name, "Jeff") self.assertEqual(self.user.last_name, "Lebowski") + self.assertEqual(self.user.phone, "123456789") def test_saving_contact_does_not_update_user_email(self): """When a contact's email is updated, the change is not propagated to the user.""" From 363fc0548d565ba27393845ca2faceb307abe67e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Apr 2024 08:37:43 -0600 Subject: [PATCH 21/76] Update get-gov-admin.js --- src/registrar/assets/js/get-gov-admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 58a47bfa5..a54720b6f 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -589,4 +589,4 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, if (toggleButton && descriptionDiv) { handleShowMoreButton(toggleButton, descriptionDiv) } -})(); \ No newline at end of file +})(); From d1e558640e3bf241818b0fa65ee2b93da3cc57c3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Apr 2024 08:57:18 -0600 Subject: [PATCH 22/76] Add waffle and cffi --- src/Pipfile | 2 + src/Pipfile.lock | 371 +++++++++++++++++-------------- src/registrar/config/settings.py | 4 + src/requirements.txt | Bin 2855 -> 6078 bytes 4 files changed, 204 insertions(+), 173 deletions(-) diff --git a/src/Pipfile b/src/Pipfile index 1565af79b..741bb16a2 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -32,6 +32,8 @@ fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"} pyzipper="*" tblib = "*" django-admin-multiple-choice-list-filter = "*" +django-waffle = "*" +cffi = "*" [dev-packages] django-debug-toolbar = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 5940f455e..3ee3dfd8a 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ce10883aef7e1ce10421d99b3ac35ebf419857a3fe468f0e2d93785f4323eaa8" + "sha256": "303753e916d4562cfa08da082bfc358d5dd3f7e6c45939225df7bd28ba3b44a9" }, "pipfile-spec": 6, "requires": {}, @@ -32,20 +32,20 @@ }, "boto3": { "hashes": [ - "sha256:168894499578a9d69d6f7deb5811952bf4171c51b95749a9aef32cf67bc71f87", - "sha256:1bd4cef11b7c5f293cede50f3d33ca89fe3413c51f1864f40163c56a732dd6b3" + "sha256:b59355bf4a1408563969526f314611dbeacc151cf90ecb22af295dcc4fe18def", + "sha256:e39516e4ca21612932599819662759c04485d53ca457996a913163da11f052a4" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.88" + "version": "==1.34.93" }, "botocore": { "hashes": [ - "sha256:36f2e9e8dfa856e55dbbe703aea601f134db3fddc3615f1020a755b27fd26a5e", - "sha256:e87a660599ed3e14b2a770f4efc3df2f2f6d04f3c7bfd64ddbae186667864a7b" + "sha256:6fbd5a53a2adc9b3d4ebd90ae0ede83a91a41d96231f8a5984051f75495f246d", + "sha256:79d39b0b87e962991c6dd55e78ce15155099f6fb741be88b1b8a456a702cc150" ], "markers": "python_version >= '3.8'", - "version": "==1.34.88" + "version": "==1.34.93" }, "cachetools": { "hashes": [ @@ -127,7 +127,8 @@ "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" ], - "markers": "platform_python_implementation != 'PyPy'", + "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==1.16.0" }, "charset-normalizer": { @@ -370,6 +371,15 @@ "markers": "python_version >= '3.8'", "version": "==7.3.0" }, + "django-waffle": { + "hashes": [ + "sha256:5979a2f3dd674ef7086480525b39651fc2045427f6d8e6a614192656d3402c5b", + "sha256:e49d7d461d89f3bd8e53f20efe39310acca8f275c9888495e68e195345bf18b1" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.1.0" + }, "django-widget-tweaks": { "hashes": [ "sha256:1c2180681ebb994e922c754804c7ffebbe1245014777ac47897a81f57cc629c7", @@ -392,12 +402,11 @@ }, "faker": { "hashes": [ - "sha256:34b947581c2bced340c39b35f89dbfac4f356932cfff8fe893bde854903f0e6e", - "sha256:adb98e771073a06bdc5d2d6710d8af07ac5da64c8dc2ae3b17bb32319e66fd82" + "sha256:13676b71346608350accc56e302d55ab7fca0db3f739145c3a3157d9623658a5", + "sha256:7692aa95155109b9348ab94afddd9049df41db64baa4ba6736653e947b52378e" ], - "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==24.11.0" + "version": "==24.14.0" }, "fred-epplib": { "git": "https://github.com/cisagov/epplib.git", @@ -801,12 +810,12 @@ }, "oic": { "hashes": [ - "sha256:385a1f64bb59519df1e23840530921bf416740240f505ea6d161e331d3d39fad", - "sha256:fcbf948a22e4d4df66f6bf57d327933f32a7b539640d9b42883457634360ba78" + "sha256:b74bd06c7de1ab4f8e798f714062e6a68f68ad9cdbed1f1c30a7fb887602f321", + "sha256:e51705d0c14c97e9ca594374bfb54269a72c9b489e0e979598344c0189bfcb64" ], "index": "pypi", - "markers": "python_version ~= '3.7'", - "version": "==1.6.1" + "markers": "python_version ~= '3.8'", + "version": "==1.7.0" }, "orderedmultidict": { "hashes": [ @@ -958,96 +967,96 @@ }, "pydantic": { "hashes": [ - "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352", - "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383" + "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5", + "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc" ], "markers": "python_version >= '3.8'", - "version": "==2.7.0" + "version": "==2.7.1" }, "pydantic-core": { "hashes": [ - "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6", - "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb", - "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0", - "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6", - "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47", - "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a", - "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a", - "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac", - "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88", - "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db", - "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d", - "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d", - "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9", - "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e", - "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b", - "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d", - "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649", - "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c", - "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1", - "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09", - "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0", - "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90", - "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d", - "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294", - "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144", - "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b", - "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1", - "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b", - "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2", - "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad", - "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622", - "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17", - "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06", - "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc", - "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50", - "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d", - "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59", - "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539", - "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a", - "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b", - "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5", - "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9", - "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278", - "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6", - "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44", - "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0", - "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb", - "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80", - "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5", - "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570", - "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b", - "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de", - "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6", - "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8", - "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203", - "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7", - "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048", - "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae", - "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89", - "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f", - "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926", - "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2", - "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76", - "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d", - "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411", - "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9", - "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2", - "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586", - "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35", - "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c", - "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143", - "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6", - "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60", - "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b", - "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226", - "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519", - "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31", - "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7", - "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b" + "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b", + "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a", + "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90", + "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d", + "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e", + "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d", + "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027", + "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804", + "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347", + "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400", + "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3", + "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399", + "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349", + "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd", + "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c", + "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e", + "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413", + "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3", + "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e", + "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3", + "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91", + "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce", + "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c", + "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb", + "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664", + "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6", + "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd", + "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3", + "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af", + "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043", + "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350", + "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7", + "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0", + "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563", + "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761", + "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72", + "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3", + "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb", + "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788", + "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b", + "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c", + "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038", + "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250", + "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec", + "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c", + "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74", + "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81", + "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439", + "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75", + "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0", + "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8", + "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150", + "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438", + "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae", + "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857", + "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038", + "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374", + "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f", + "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241", + "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592", + "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4", + "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d", + "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b", + "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b", + "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182", + "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e", + "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641", + "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70", + "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9", + "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a", + "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543", + "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b", + "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f", + "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38", + "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845", + "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2", + "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0", + "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4", + "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242" ], "markers": "python_version >= '3.8'", - "version": "==2.18.1" + "version": "==2.18.2" }, "pydantic-settings": { "hashes": [ @@ -1148,6 +1157,14 @@ "markers": "python_version >= '3.8'", "version": "==4.11.0" }, + "tzdata": { + "hashes": [ + "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", + "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" + ], + "markers": "sys_platform == 'win32'", + "version": "==2024.1" + }, "urllib3": { "hashes": [ "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", @@ -1244,32 +1261,32 @@ }, "black": { "hashes": [ - "sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d", - "sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd", - "sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33", - "sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965", - "sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070", - "sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397", - "sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745", - "sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1", - "sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665", - "sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436", - "sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb", - "sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e", - "sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6", - "sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702", - "sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8", - "sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8", - "sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3", - "sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad", - "sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf", - "sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e", - "sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641", - "sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2" + "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474", + "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1", + "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0", + "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8", + "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96", + "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1", + "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04", + "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021", + "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94", + "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d", + "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c", + "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7", + "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c", + "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc", + "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7", + "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d", + "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c", + "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741", + "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce", + "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb", + "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063", + "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==24.4.0" + "version": "==24.4.2" }, "blinker": { "hashes": [ @@ -1281,12 +1298,12 @@ }, "boto3": { "hashes": [ - "sha256:168894499578a9d69d6f7deb5811952bf4171c51b95749a9aef32cf67bc71f87", - "sha256:1bd4cef11b7c5f293cede50f3d33ca89fe3413c51f1864f40163c56a732dd6b3" + "sha256:b59355bf4a1408563969526f314611dbeacc151cf90ecb22af295dcc4fe18def", + "sha256:e39516e4ca21612932599819662759c04485d53ca457996a913163da11f052a4" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.88" + "version": "==1.34.93" }, "boto3-mocking": { "hashes": [ @@ -1299,28 +1316,28 @@ }, "boto3-stubs": { "hashes": [ - "sha256:23ca9e0cd0d3e7702d6631a1e94a4208a26b39fa6b12c734427e68a7fa649477", - "sha256:8f472d1bf09743c3d33304ecc8830d70ebe3ca19ac9604ae8da9af55421b0fce" + "sha256:13c86a8137e969cea21a0f5700c66eb74f864cba8b94a816ee66b4224234f645", + "sha256:1aae0f06c56c8d9a67c45afaf61f2e7bc3fefac207ba5946a23b562d2631d8a3" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.88" + "version": "==1.34.93" }, "botocore": { "hashes": [ - "sha256:36f2e9e8dfa856e55dbbe703aea601f134db3fddc3615f1020a755b27fd26a5e", - "sha256:e87a660599ed3e14b2a770f4efc3df2f2f6d04f3c7bfd64ddbae186667864a7b" + "sha256:6fbd5a53a2adc9b3d4ebd90ae0ede83a91a41d96231f8a5984051f75495f246d", + "sha256:79d39b0b87e962991c6dd55e78ce15155099f6fb741be88b1b8a456a702cc150" ], "markers": "python_version >= '3.8'", - "version": "==1.34.88" + "version": "==1.34.93" }, "botocore-stubs": { "hashes": [ - "sha256:656e966ea152a4f2828892aa7a9673bc91799998f5a8efd8e8fe390f61c2f4f1", - "sha256:f55b03ae2e1706bd56299fd2975bb048f96aa49012a866e931a040a74f85c3cc" + "sha256:22e71e4c507df2e91d5673d2b65281c115d4c8d2601a3c2497c7a309fef6a0ec", + "sha256:909ebaf7a70ce83de4d85eed4b810e68c4d4cb64606d7431a263b909c92b7fa4" ], "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==1.34.88" + "version": "==1.34.93" }, "click": { "hashes": [ @@ -1330,6 +1347,14 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "platform_system == 'Windows'", + "version": "==0.4.6" + }, "django": { "hashes": [ "sha256:a2d4c4d4ea0b6f0895acde632071aff6400bfc331228fc978b05452a0ff3e9f1", @@ -1423,37 +1448,37 @@ }, "mypy": { "hashes": [ - "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6", - "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913", - "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129", - "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc", - "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974", - "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374", - "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150", - "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03", - "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9", - "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02", - "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89", - "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2", - "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d", - "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3", - "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612", - "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e", - "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3", - "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e", - "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd", - "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04", - "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed", - "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185", - "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf", - "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b", - "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4", - "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f", - "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6" + "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061", + "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99", + "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de", + "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a", + "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9", + "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec", + "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1", + "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131", + "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f", + "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821", + "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5", + "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee", + "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e", + "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746", + "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2", + "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0", + "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b", + "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53", + "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30", + "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda", + "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051", + "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2", + "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7", + "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee", + "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727", + "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976", + "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.9.0" + "version": "==1.10.0" }, "mypy-extensions": { "hashes": [ @@ -1497,11 +1522,11 @@ }, "platformdirs": { "hashes": [ - "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", - "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf", + "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1" ], "markers": "python_version >= '3.8'", - "version": "==4.2.0" + "version": "==4.2.1" }, "pycodestyle": { "hashes": [ @@ -1640,14 +1665,6 @@ "markers": "python_version >= '3.8'", "version": "==5.2.0" }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, "types-awscrt": { "hashes": [ "sha256:3ae374b553e7228ba41a528cf42bd0b2ad7303d806c73eff4aaaac1515e3ea4e", @@ -1707,6 +1724,14 @@ "markers": "python_version >= '3.8'", "version": "==4.11.0" }, + "tzdata": { + "hashes": [ + "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", + "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" + ], + "markers": "sys_platform == 'win32'", + "version": "==2024.1" + }, "urllib3": { "hashes": [ "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 54b65e83e..9b854d906 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -148,6 +148,8 @@ INSTALLED_APPS = [ "corsheaders", # library for multiple choice filters in django admin "django_admin_multiple_choice_list_filter", + # Waffle feature flags + "waffle", ] # Middleware are routines for processing web requests. @@ -183,6 +185,8 @@ MIDDLEWARE = [ "csp.middleware.CSPMiddleware", # django-auditlog: obtain the request User for use in logging "auditlog.middleware.AuditlogMiddleware", + # Used for waffle feature flags + "waffle.middleware.WaffleMiddleware", ] # application object used by Django’s built-in servers (e.g. `runserver`) diff --git a/src/requirements.txt b/src/requirements.txt index 3c005f1622d30e48850abcdfb944686e8b12175c..0a79d1f59a6b0ee6aef7ba94f297f8d81e9e5752 100644 GIT binary patch literal 6078 zcmchb+iu%N5Qg`uO zN&1va@O}6$+-Y>J(Morfp5>vVD>4er{Z&U#Z%=gg!Y^76`7ZTMc&k~vD3evL!f%>? z6<&p*=HKXQE0kJ!p?O7^MTs`r4gHo9r%~UHRzbhM)*6P9es6Vcr=v^EB2L8@lbZ?E zOg<=t*+$25kLft-jNWPbUhgr@BJPz?o#_OHf&4RwC=4WfkSKR@kUNv)wfyRyH%;+l zrnP)EFz+9+Vj1}}i}1oai0*dM>Te37hipPSz}=WMra6N<5(pa%+q~9od#6t|H1F z;l1#q-Uq$@;cjra8y@aPdvx;fAW!5%1^-WVVo669?e>GOe`MsDkl_kW} zN_`tSp_F!HxVQS>l-AwLdaZTF%80pexDjt=x=wVAbVrAj>FnwEPMy+q9J|wV{&5}Y z-I>OzF-YlaCOL*SJJ{ zi$o&Iq*}BRwP`Ws`FAcn*YdUH(e++aP)bD>Oo!B(=^ z1$bmzWlNLkx-A=>3>m`ecA6~jU+M226(Oz{wx)W*GY+zLlJ+EPi99-E_W_jI+Soj5}#Bzx685B)~A=epGnuc~k& z6JF#fs;T!QbPB22)44(S{9z@nt0b0MT{ll#M}oiJ#|YX=i-B_QG-`xDtrv%5RPWnF zoNK+1kHxmbu>KCI10A17{MhNO*7w_D!8TVh|Y}3t+PKFy`OOtawmg^eEZv5IjzakU^}d z%{`d!lnP}a^sv1iI#}Py7oJU&lM-Fuf)Q37Pi^5FlXY=!cblr=`3dPSLWQ2n`!(c| z2PTOT=*%V8(fyluYM&2dPEJADYOLWy-mz+w`u*?Qv8V@^`NW+&%O!dDr)xM@$9$^>oZXX9= zY%@@I-5Oy8y0wtuVvyp`-;Q}3b5f4f5Jx@&1Mc1^Nkt$uQxIxW4+MeD{%4~ zIQcw%p5UWngA<-;7&9eb#1Qx?ez>+7m3l+wk?!#2Fj;Tff!S`Vs(nroMk8)5^F1*&3zB-YFvI3Uu)-49C6kxnp<`VJ^SN5U6s{sy9UHkSSU zf8+WP2HN0$g1`WC5Z^}h2q(SdT!||S)xD#qNM)dnSTv!~V}BL{@C(Ji{BYuw^y06o zIxYbG9@JIx7M16(f?a?lLm8)7!AB@{IT-D}%#d(P7V^39MZ+#3JLsZdndYbofQ7Y6 zo3_fL4t{sznbxM?*3z-oJZ)z)iZ`G5q2$>^(PU-0Z;E}r-_`7N%t}_U6D#(I zW+$3MnboW2wIB=p_T#J=snBy;{$vYmR;_qKYe%T4=`_-I5JD>fOBCRe2(_b@s51?h zp|4Zqp|2&^EVw%+Zar*PkG*h~`7Wdwt&%W=FBR3{5c)D<1kvl+1Dh1@ ztz!+!Hz>h^Fu~MykUn&6z>%Ys6PLp;dOzG*_%r+|A{SvS?jwmCUV%?7A$j@CRS!bK={s%?<>&;>M4O%mXOVN`Q& zdhr}-NG1K371~amX&?B5izNIQ%WO#I*^2ZnPV2j0f+^;~E|dgV-$~Hx9$=@(>qWSj z7z_b`51U1pA7b<}&JGA3wZ{4P3;*Xi2n^4c7$0Z$QYS2c}(f8(8T^#Py(Z#B7i2N_m1VFSLY{D45r^BclA*w(u)txo;}g?Y!H( zpM<#5`7=a&R Date: Tue, 30 Apr 2024 09:28:11 -0600 Subject: [PATCH 23/76] Add basic flag --- src/registrar/config/settings.py | 7 +++++++ src/registrar/templates/base.html | 3 +++ src/registrar/templates/includes/non-production-alert.html | 3 +-- src/registrar/views/index.py | 6 +++++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 9b854d906..885d95b75 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -323,6 +323,13 @@ EMAIL_TIMEOUT = 30 SERVER_EMAIL = "root@get.gov" # endregion + +# region: Waffle feature flags-----------------------------------------------------------### +# If Waffle encounters a reference to a flag that is not in the database, should Waffle create the flag? +# WAFFLE_CREATE_MISSING_FLAGS + +# endregion + # region: Headers-----------------------------------------------------------### # Content-Security-Policy configuration diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index c0702e78f..814ea2656 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -72,6 +72,9 @@ {% if not IS_PRODUCTION %} {% include "includes/non-production-alert.html" %} + {% if profile_feature_flag %} +

it worked!

+ {% endif %} {% endif %}
diff --git a/src/registrar/templates/includes/non-production-alert.html b/src/registrar/templates/includes/non-production-alert.html index 911eea9d6..efea5cdea 100644 --- a/src/registrar/templates/includes/non-production-alert.html +++ b/src/registrar/templates/includes/non-production-alert.html @@ -1,5 +1,4 @@ -
+
Attention: You are on a test site. diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py index cb170078a..a715c3cb2 100644 --- a/src/registrar/views/index.py +++ b/src/registrar/views/index.py @@ -1,7 +1,7 @@ from django.shortcuts import render from registrar.models import DomainRequest, Domain, UserDomainRole - +from waffle.decorators import flag_is_active def index(request): """This page is available to anyone without logging in.""" @@ -20,6 +20,10 @@ def index(request): has_deletable_domain_requests = deletable_domain_requests.exists() context["has_deletable_domain_requests"] = has_deletable_domain_requests + # This is a django waffle flag which toggles features based off of the "flag" table + # TODO - note that the flag must be checked for superuser AND staff for superuser to see it + context["profile_feature_flag"] = flag_is_active(request, "profile_feature") + # If they can delete domain requests, add the delete button to the context if has_deletable_domain_requests: # Add the delete modal button to the context From 5e2ec5d1c87f8844b70ce4b59ac3bc3783f24749 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:52:53 -0600 Subject: [PATCH 24/76] Migrations, etc --- src/registrar/admin.py | 16 ++++++++ src/registrar/config/settings.py | 6 ++- ...py => 0089_user_verification_type copy.py} | 0 src/registrar/migrations/0090_waffleflag.py | 41 +++++++++++++++++++ src/registrar/models/__init__.py | 3 ++ src/registrar/models/waffle_flag.py | 37 +++++++++++++++++ src/registrar/templates/base.html | 2 +- src/registrar/views/index.py | 2 +- 8 files changed, 103 insertions(+), 4 deletions(-) rename src/registrar/migrations/{0089_user_verification_type.py => 0089_user_verification_type copy.py} (100%) create mode 100644 src/registrar/migrations/0090_waffleflag.py create mode 100644 src/registrar/models/waffle_flag.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 485751b3c..d06714de2 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -15,7 +15,9 @@ from django.contrib.contenttypes.models import ContentType from django.urls import reverse from dateutil.relativedelta import relativedelta # type: ignore from epplibwrapper.errors import ErrorCode, RegistryError +from waffle.admin import FlagAdmin from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website +from waffle.models import Sample, Switch from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.views.utility.mixins import OrderableFieldsMixin from django.contrib.admin.views.main import ORDER_VAR @@ -2157,8 +2159,19 @@ class UserGroupAdmin(AuditedAdmin): def user_group(self, obj): return obj.name +class WaffleFlagAdmin(FlagAdmin): + class Meta: + """Contains meta information about this class""" + model = models.WaffleFlag + fields = "__all__" admin.site.unregister(LogEntry) # Unregister the default registration + +# Unregister samples and switches from django-waffle, as we currently don't use these. +# TODO - address this +admin.site.unregister(Sample) +admin.site.unregister(Switch) + admin.site.register(LogEntry, CustomLogEntryAdmin) admin.site.register(models.User, MyUserAdmin) # Unregister the built-in Group model @@ -2180,3 +2193,6 @@ admin.site.register(models.PublicContact, PublicContactAdmin) admin.site.register(models.DomainRequest, DomainRequestAdmin) admin.site.register(models.TransitionDomain, TransitionDomainAdmin) admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) + +# Register our custom waffle flag implementation +admin.site.register(models.WaffleFlag, WaffleFlagAdmin) \ No newline at end of file diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 885d95b75..96663adf9 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -22,7 +22,6 @@ from base64 import b64decode from cfenv import AppEnv # type: ignore from pathlib import Path from typing import Final - from botocore.config import Config # # # ### @@ -326,7 +325,10 @@ SERVER_EMAIL = "root@get.gov" # region: Waffle feature flags-----------------------------------------------------------### # If Waffle encounters a reference to a flag that is not in the database, should Waffle create the flag? -# WAFFLE_CREATE_MISSING_FLAGS +WAFFLE_CREATE_MISSING_FLAGS = False + +# The model that will be used to keep track of flags. Extends AbstractUserFlag. +WAFFLE_FLAG_MODEL = "registrar.WaffleFlag" # endregion diff --git a/src/registrar/migrations/0089_user_verification_type.py b/src/registrar/migrations/0089_user_verification_type copy.py similarity index 100% rename from src/registrar/migrations/0089_user_verification_type.py rename to src/registrar/migrations/0089_user_verification_type copy.py diff --git a/src/registrar/migrations/0090_waffleflag.py b/src/registrar/migrations/0090_waffleflag.py new file mode 100644 index 000000000..d8e6fb229 --- /dev/null +++ b/src/registrar/migrations/0090_waffleflag.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.10 on 2024-04-30 16:25 + +from django.conf import settings +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('registrar', '0089_user_verification_type copy'), + ] + + operations = [ + migrations.CreateModel( + name='WaffleFlag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='The human/computer readable name.', max_length=100, unique=True, verbose_name='Name')), + ('everyone', models.BooleanField(blank=True, help_text='Flip this flag on (Yes) or off (No) for everyone, overriding all other settings. Leave as Unknown to use normally.', null=True, verbose_name='Everyone')), + ('percent', models.DecimalField(blank=True, decimal_places=1, help_text='A number between 0.0 and 99.9 to indicate a percentage of users for whom this flag will be active.', max_digits=3, null=True, verbose_name='Percent')), + ('testing', models.BooleanField(default=False, help_text='Allow this flag to be set for a session for user testing', verbose_name='Testing')), + ('superusers', models.BooleanField(default=True, help_text='Flag always active for superusers?', verbose_name='Superusers')), + ('staff', models.BooleanField(default=False, help_text='Flag always active for staff?', verbose_name='Staff')), + ('authenticated', models.BooleanField(default=False, help_text='Flag always active for authenticated users?', verbose_name='Authenticated')), + ('languages', models.TextField(blank=True, default='', help_text='Activate this flag for users with one of these languages (comma-separated list)', verbose_name='Languages')), + ('rollout', models.BooleanField(default=False, help_text='Activate roll-out mode?', verbose_name='Rollout')), + ('note', models.TextField(blank=True, help_text='Note where this Flag is used.', verbose_name='Note')), + ('created', models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='Date when this Flag was created.', verbose_name='Created')), + ('modified', models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this Flag was last modified.', verbose_name='Modified')), + ('groups', models.ManyToManyField(blank=True, help_text='Activate this flag for these user groups.', to='auth.group', verbose_name='Groups')), + ('users', models.ManyToManyField(blank=True, help_text='Activate this flag for these users.', to=settings.AUTH_USER_MODEL, verbose_name='Users')), + ], + options={ + 'verbose_name': 'Flag', + 'verbose_name_plural': 'Flags', + 'abstract': False, + }, + ), + ] diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index d3bbb3ae5..aac9e09b6 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -15,6 +15,7 @@ from .user_group import UserGroup from .website import Website from .transition_domain import TransitionDomain from .verified_by_staff import VerifiedByStaff +from .waffle_flag import WaffleFlag __all__ = [ "Contact", @@ -33,6 +34,7 @@ __all__ = [ "Website", "TransitionDomain", "VerifiedByStaff", + "WaffleFlag", ] auditlog.register(Contact) @@ -51,3 +53,4 @@ auditlog.register(UserGroup, m2m_fields=["permissions"]) auditlog.register(Website) auditlog.register(TransitionDomain) auditlog.register(VerifiedByStaff) +auditlog.register(WaffleFlag) \ No newline at end of file diff --git a/src/registrar/models/waffle_flag.py b/src/registrar/models/waffle_flag.py new file mode 100644 index 000000000..827947cb3 --- /dev/null +++ b/src/registrar/models/waffle_flag.py @@ -0,0 +1,37 @@ +from waffle.models import AbstractUserFlag +import logging + +logger = logging.getLogger(__name__) + + +class WaffleFlag(AbstractUserFlag): + """ + Custom implementation of django-waffles 'Flag' object. + Read more here: https://waffle.readthedocs.io/en/stable/types/flag.html + + Use this class when dealing with feature flags, such as profile_feature. + """ + + class Meta: + """Contains meta information about this class""" + verbose_name = "waffle flag" + verbose_name_plural = "Waffle flags" + + @classmethod + def create_waffle_flags(cls): + """ + Creates a pre-defined list of flags for our migrations. + """ + logger.info("Creating default waffle flags...") + try: + # Flags can be activated through the command line or through django admin. + # To keep the scope of this function minimal and simple, if we require additional + # config on these flag, it should be done in a seperate function or as a command. + flag_names = [ + "profile_feature", + "dns_hosting_feature", + ] + flags = [cls(name=flag_name) for flag_name in flag_names] + cls.objects.bulk_create(flags) + except Exception as e: + logger.error(f"An error occurred when attempting to create WaffleFlags: {e}") \ No newline at end of file diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index 814ea2656..9a801f274 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -72,7 +72,7 @@ {% if not IS_PRODUCTION %} {% include "includes/non-production-alert.html" %} - {% if profile_feature_flag %} + {% if has_profile_feature_flag %}

it worked!

{% endif %} {% endif %} diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py index a715c3cb2..cb5fb79fa 100644 --- a/src/registrar/views/index.py +++ b/src/registrar/views/index.py @@ -22,7 +22,7 @@ def index(request): # This is a django waffle flag which toggles features based off of the "flag" table # TODO - note that the flag must be checked for superuser AND staff for superuser to see it - context["profile_feature_flag"] = flag_is_active(request, "profile_feature") + context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") # If they can delete domain requests, add the delete button to the context if has_deletable_domain_requests: From 91545f45beeeb95b428465e178b50e2427bf3603 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Apr 2024 11:22:25 -0600 Subject: [PATCH 25/76] Add waffle flags to migrations --- docs/developer/feature-flags.md | 0 src/registrar/migrations/0090_waffleflag.py | 7 ++- .../0091_create_waffle_flags_v01.py | 38 ++++++++++++++ src/registrar/models/waffle_flag.py | 50 ++++++++++++++----- src/registrar/templates/base.html | 3 -- .../includes/non-production-alert.html | 3 ++ 6 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 docs/developer/feature-flags.md create mode 100644 src/registrar/migrations/0091_create_waffle_flags_v01.py diff --git a/docs/developer/feature-flags.md b/docs/developer/feature-flags.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/registrar/migrations/0090_waffleflag.py b/src/registrar/migrations/0090_waffleflag.py index d8e6fb229..ff449d77a 100644 --- a/src/registrar/migrations/0090_waffleflag.py +++ b/src/registrar/migrations/0090_waffleflag.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-04-30 16:25 +# Generated by Django 4.2.10 on 2024-04-30 16:56 from django.conf import settings from django.db import migrations, models @@ -33,9 +33,8 @@ class Migration(migrations.Migration): ('users', models.ManyToManyField(blank=True, help_text='Activate this flag for these users.', to=settings.AUTH_USER_MODEL, verbose_name='Users')), ], options={ - 'verbose_name': 'Flag', - 'verbose_name_plural': 'Flags', - 'abstract': False, + 'verbose_name': 'waffle flag', + 'verbose_name_plural': 'Waffle flags', }, ), ] diff --git a/src/registrar/migrations/0091_create_waffle_flags_v01.py b/src/registrar/migrations/0091_create_waffle_flags_v01.py new file mode 100644 index 000000000..c98542a14 --- /dev/null +++ b/src/registrar/migrations/0091_create_waffle_flags_v01.py @@ -0,0 +1,38 @@ +# This migration creates default WaffleFlag objects for our DB. +# Whenever you add to the `create_waffle_flags` function, increment/copy this +# migration by one + +from django.db import migrations +from registrar.models import WaffleFlag +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_flags(): + """ + Populates pre-existing flags we wish to associate. + Only generates a flag name and a note, but no other data is loaded at this point. + """ + WaffleFlag.create_waffle_flags_for_migrations() + +def delete_flags(): + """ + Deletes all prexisting flags. + Does not delete flags not defined in this scope (user generated). + """ + WaffleFlag.delete_waffle_flags_for_migrations() + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0090_waffleflag"), + ] + + operations = [ + migrations.RunPython( + code=create_flags, + reverse_code=delete_flags, + atomic=True, + ), + ] diff --git a/src/registrar/models/waffle_flag.py b/src/registrar/models/waffle_flag.py index 827947cb3..8fe710dbd 100644 --- a/src/registrar/models/waffle_flag.py +++ b/src/registrar/models/waffle_flag.py @@ -17,21 +17,45 @@ class WaffleFlag(AbstractUserFlag): verbose_name = "waffle flag" verbose_name_plural = "Waffle flags" + # Defines which waffle flags should be created at startup. + # Add to this list if you want to add another flag that is generated at startup. + # When you do so, you will need to add a new instance of `0091_create_waffle_flags_v{version_number}` + # in registrar/migrations for that change to update automatically on migrate. + DEFAULT_WAFFLE_FLAGS = [ + "profile_feature", + "dns_hosting_feature" + ] + @classmethod - def create_waffle_flags(cls): + def create_waffle_flags_for_migrations(cls): """ Creates a pre-defined list of flags for our migrations. """ logger.info("Creating default waffle flags...") - try: - # Flags can be activated through the command line or through django admin. - # To keep the scope of this function minimal and simple, if we require additional - # config on these flag, it should be done in a seperate function or as a command. - flag_names = [ - "profile_feature", - "dns_hosting_feature", - ] - flags = [cls(name=flag_name) for flag_name in flag_names] - cls.objects.bulk_create(flags) - except Exception as e: - logger.error(f"An error occurred when attempting to create WaffleFlags: {e}") \ No newline at end of file + # Flags can be changed through the command line or through django admin. + # To keep the scope of this function minimal and simple, if we require additional + # config on these flag, it should be done in a seperate function or as a command. + for flag_name in cls.DEFAULT_WAFFLE_FLAGS: + try: + cls.objects.update_or_create( + name=flag_name, + # Booleans like superusers or is_staff can be set here, if needed. + defaults={ + 'note': 'Auto-generated waffle flag' + } + ) + except Exception as e: + logger.error(f"An error occurred when attempting to add or update flag {flag_name}: {e}") + + @classmethod + def delete_waffle_flags_for_migrations(cls): + """ + Delete a pre-defined list of flags for our migrations (the reverse_code operation). + """ + logger.info("Deleting default waffle flags...") + existing_flags = cls.objects.filter(name__in=cls.DEFAULT_WAFFLE_FLAGS) + for flag in existing_flags: + try: + cls.objects.get(name=flag.name).delete() + except Exception as e: + logger.error(f"An error occurred when attempting to delete flag {flag.name}: {e}") \ No newline at end of file diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index 9a801f274..c0702e78f 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -72,9 +72,6 @@ {% if not IS_PRODUCTION %} {% include "includes/non-production-alert.html" %} - {% if has_profile_feature_flag %} -

it worked!

- {% endif %} {% endif %}
diff --git a/src/registrar/templates/includes/non-production-alert.html b/src/registrar/templates/includes/non-production-alert.html index efea5cdea..ca7337777 100644 --- a/src/registrar/templates/includes/non-production-alert.html +++ b/src/registrar/templates/includes/non-production-alert.html @@ -2,6 +2,9 @@
Attention: You are on a test site. + {% if has_profile_feature_flag %} + The profile_feature flag is active. + {% endif %}
\ No newline at end of file From c6b6c17540ff91a8b37d15e92454790806a85038 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 30 Apr 2024 13:28:03 -0400 Subject: [PATCH 26/76] revise logic a bit to ensure user and contact do not overwrite each other --- src/djangooidc/backends.py | 21 +++--------- src/djangooidc/tests/test_backends.py | 49 +++------------------------ src/registrar/models/contact.py | 23 +++++++++---- 3 files changed, 26 insertions(+), 67 deletions(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 6556d8d7a..9b500cd3f 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -8,8 +8,6 @@ from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend from django.utils import timezone -from registrar.models.contact import Contact - logger = logging.getLogger(__name__) @@ -79,26 +77,15 @@ class OpenIdConnectBackend(ModelBackend): 'last_name', and 'phone' fields, unless specific conditions are met. - 'phone' field will be updated if it's None or an empty string. - - 'first_name' and 'last_name' will be updated if the provided value is not empty. + - 'first_name', 'last_name' or 'phone' will be updated if the provided value is not empty. """ - contacts = Contact.objects.filter(user=user) - - if len(contacts) == 0: # no matching contact - logger.warning("Could not find a contact when one should've existed.") - - if len(contacts) > 1: # multiple matches - logger.warning("There are multiple Contacts with the same email address.") - # Iterate over fields to update for key, value in kwargs.items(): # Check if the field is not 'first_name', 'last_name', or 'phone', - # or if it's the 'phone' field and 'user.phone' is None or empty, - # or if it's 'first_name' or 'last_name' and the provided value is not empty - if ( - key not in ["first_name", "last_name", "phone"] - or (key == "phone" and not contacts[0].phone) - or (key in ["first_name", "last_name"] and value) + # or if it's 'first_name' or 'last_name' or 'phone' and the provided value is not empty + if key not in ["first_name", "last_name", "phone"] or ( + key in ["first_name", "last_name", "phone"] and value ): # Update the corresponding attribute of the user object setattr(user, key, value) diff --git a/src/djangooidc/tests/test_backends.py b/src/djangooidc/tests/test_backends.py index ffe26e2cb..c15106fa9 100644 --- a/src/djangooidc/tests/test_backends.py +++ b/src/djangooidc/tests/test_backends.py @@ -64,6 +64,7 @@ class OpenIdConnectBackendTestCase(TestCase): # Remove given_name and family_name from the input, self.kwargs self.kwargs.pop("given_name", None) self.kwargs.pop("family_name", None) + self.kwargs.pop("phone", None) # Ensure that the authenticate method updates the existing user # and preserves existing first and last names @@ -78,53 +79,13 @@ class OpenIdConnectBackendTestCase(TestCase): self.assertEqual(user.email, "john.doe@example.com") self.assertEqual(user.phone, "9999999999") - def test_authenticate_with_existing_user_with_no_phone_will_update_phone(self): - """Test that authenticate updates an existing user if it finds one. - For this test, the existing user has an empty string for phone - - The existing user's phone number is overwritten""" - # Create an existing user with the same username and with first and last names - existing_user = User.objects.create_user(username="test_user", phone="") - - # Ensure that the authenticate method updates the existing user - # and preserves existing first and last names - user = self.backend.authenticate(request=None, **self.kwargs) - self.assertIsNotNone(user) - self.assertIsInstance(user, User) - self.assertEqual(user, existing_user) # The same user instance should be returned - - # Verify that user fields are correctly updated - self.assertEqual(user.first_name, "John") - self.assertEqual(user.last_name, "Doe") - self.assertEqual(user.email, "john.doe@example.com") - self.assertEqual(user.phone, "123456789") - - def test_authenticate_with_existing_user_with_none_phone_will_update_phone(self): - """Test that authenticate updates an existing user if it finds one. - For this test, the existing user has None for phone - - The existing user's phone number is overwritten""" - # Create an existing user with the same username and with first and last names - existing_user = User.objects.create_user(username="test_user") - - # Ensure that the authenticate method updates the existing user - # and preserves existing first and last names - user = self.backend.authenticate(request=None, **self.kwargs) - self.assertIsNotNone(user) - self.assertIsInstance(user, User) - self.assertEqual(user, existing_user) # The same user instance should be returned - - # Verify that user fields are correctly updated - self.assertEqual(user.first_name, "John") - self.assertEqual(user.last_name, "Doe") - self.assertEqual(user.email, "john.doe@example.com") - self.assertEqual(user.phone, "123456789") - - def test_authenticate_with_existing_user_different_name(self): + def test_authenticate_with_existing_user_different_name_phone(self): """Test that authenticate updates an existing user if it finds one. For this test, given_name and family_name are supplied and overwrite""" # Create an existing user with the same username and with first and last names - existing_user = User.objects.create_user(username="test_user", first_name="WillBe", last_name="Replaced") + existing_user = User.objects.create_user( + username="test_user", first_name="WillBe", last_name="Replaced", phone="987654321" + ) # Ensure that the authenticate method updates the existing user # and preserves existing first and last names diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 185b84ded..3ebd8bc3e 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -101,12 +101,23 @@ class Contact(TimeStampedModel): # Call the parent class's save method to perform the actual save super().save(*args, **kwargs) - # Update the related User object's first_name and last_name - if self.user and (not self.user.first_name or not self.user.last_name or not self.user.phone): - self.user.first_name = self.first_name - self.user.last_name = self.last_name - self.user.phone = self.phone - self.user.save() + if self.user: + updated = False + + # Update first name and last name if necessary + if not self.user.first_name or not self.user.last_name: + self.user.first_name = self.first_name + self.user.last_name = self.last_name + updated = True + + # Update phone if necessary + if not self.user.phone: + self.user.phone = self.phone + updated = True + + # Save user if any updates were made + if updated: + self.user.save() def __str__(self): if self.first_name or self.last_name: From 7160efa51536f3ecd0f0a08c9fb597f66774d1c2 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 30 Apr 2024 13:30:26 -0400 Subject: [PATCH 27/76] comment --- src/djangooidc/backends.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 9b500cd3f..81e782c7b 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -76,7 +76,6 @@ class OpenIdConnectBackend(ModelBackend): This method updates user fields while preserving the values of 'first_name', 'last_name', and 'phone' fields, unless specific conditions are met. - - 'phone' field will be updated if it's None or an empty string. - 'first_name', 'last_name' or 'phone' will be updated if the provided value is not empty. """ From 74978ba5ba002ccff5da8f1ad97a37c2f8a0d37b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:26:46 -0600 Subject: [PATCH 28/76] Cleanup --- src/Pipfile | 1 - src/Pipfile.lock | 107 ++++++--------- src/registrar/admin.py | 5 +- src/registrar/migrations/0090_waffleflag.py | 127 +++++++++++++++--- .../0091_create_waffle_flags_v01.py | 16 ++- src/registrar/models/__init__.py | 2 +- src/registrar/models/waffle_flag.py | 50 ++++--- src/requirements.txt | Bin 6078 -> 2900 bytes 8 files changed, 193 insertions(+), 115 deletions(-) diff --git a/src/Pipfile b/src/Pipfile index 741bb16a2..8d94a2308 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -33,7 +33,6 @@ pyzipper="*" tblib = "*" django-admin-multiple-choice-list-filter = "*" django-waffle = "*" -cffi = "*" [dev-packages] django-debug-toolbar = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 3ee3dfd8a..21ce32516 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "303753e916d4562cfa08da082bfc358d5dd3f7e6c45939225df7bd28ba3b44a9" + "sha256": "2c6a2a75bb42b1c2c4eb57e472ff7da8e9f2908e0b71cfb01daab53c73d26964" }, "pipfile-spec": 6, "requires": {}, @@ -32,20 +32,20 @@ }, "boto3": { "hashes": [ - "sha256:b59355bf4a1408563969526f314611dbeacc151cf90ecb22af295dcc4fe18def", - "sha256:e39516e4ca21612932599819662759c04485d53ca457996a913163da11f052a4" + "sha256:22f65b3c9b7a419f8f39c2dddc421e14fab8cbb3bd8a9d467e874237d39f59b1", + "sha256:bbb87d641c73462e53b1777083b55c8f13921618ad08757478a8122985c56c13" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.93" + "version": "==1.34.94" }, "botocore": { "hashes": [ - "sha256:6fbd5a53a2adc9b3d4ebd90ae0ede83a91a41d96231f8a5984051f75495f246d", - "sha256:79d39b0b87e962991c6dd55e78ce15155099f6fb741be88b1b8a456a702cc150" + "sha256:99b11be9a28f9051af4c96fa121e9c3f22a86d499abd773c9e868b2a38961bae", + "sha256:f00a79002e0cb9d6895ecd0919c506402850177d7b6c4d2634fa2da362d95bcb" ], "markers": "python_version >= '3.8'", - "version": "==1.34.93" + "version": "==1.34.94" }, "cachetools": { "hashes": [ @@ -127,8 +127,7 @@ "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" ], - "index": "pypi", - "markers": "python_version >= '3.8'", + "markers": "platform_python_implementation != 'PyPy'", "version": "==1.16.0" }, "charset-normalizer": { @@ -402,11 +401,12 @@ }, "faker": { "hashes": [ - "sha256:13676b71346608350accc56e302d55ab7fca0db3f739145c3a3157d9623658a5", - "sha256:7692aa95155109b9348ab94afddd9049df41db64baa4ba6736653e947b52378e" + "sha256:87ef41e24b39a5be66ecd874af86f77eebd26782a2681200e86c5326340a95d3", + "sha256:e23a2b74888885c3d23a9237bacb823041291c03d609a39acb9ebe6c123f3986" ], + "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==24.14.0" + "version": "==25.0.0" }, "fred-epplib": { "git": "https://github.com/cisagov/epplib.git", @@ -597,7 +597,6 @@ "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b", "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6", "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8", - "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5", "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306", "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5", "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f", @@ -1157,14 +1156,6 @@ "markers": "python_version >= '3.8'", "version": "==4.11.0" }, - "tzdata": { - "hashes": [ - "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", - "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" - ], - "markers": "sys_platform == 'win32'", - "version": "==2024.1" - }, "urllib3": { "hashes": [ "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", @@ -1290,20 +1281,20 @@ }, "blinker": { "hashes": [ - "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9", - "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182" + "sha256:5f1cdeff423b77c31b89de0565cd03e5275a03028f44b2b15f912632a58cced6", + "sha256:da44ec748222dcd0105ef975eed946da197d5bdf8bafb6aa92f5bc89da63fa25" ], "markers": "python_version >= '3.8'", - "version": "==1.7.0" + "version": "==1.8.1" }, "boto3": { "hashes": [ - "sha256:b59355bf4a1408563969526f314611dbeacc151cf90ecb22af295dcc4fe18def", - "sha256:e39516e4ca21612932599819662759c04485d53ca457996a913163da11f052a4" + "sha256:22f65b3c9b7a419f8f39c2dddc421e14fab8cbb3bd8a9d467e874237d39f59b1", + "sha256:bbb87d641c73462e53b1777083b55c8f13921618ad08757478a8122985c56c13" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.93" + "version": "==1.34.94" }, "boto3-mocking": { "hashes": [ @@ -1316,28 +1307,28 @@ }, "boto3-stubs": { "hashes": [ - "sha256:13c86a8137e969cea21a0f5700c66eb74f864cba8b94a816ee66b4224234f645", - "sha256:1aae0f06c56c8d9a67c45afaf61f2e7bc3fefac207ba5946a23b562d2631d8a3" + "sha256:6722b0b024293eb37713b8ec35d02e2c42c48b43615a8544c402972db053412d", + "sha256:6dc13d312ea2e7e045e71ba8d2796ee41e1ba1b98eaef7a84eb099e5b46ee450" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.93" + "version": "==1.34.94" }, "botocore": { "hashes": [ - "sha256:6fbd5a53a2adc9b3d4ebd90ae0ede83a91a41d96231f8a5984051f75495f246d", - "sha256:79d39b0b87e962991c6dd55e78ce15155099f6fb741be88b1b8a456a702cc150" + "sha256:99b11be9a28f9051af4c96fa121e9c3f22a86d499abd773c9e868b2a38961bae", + "sha256:f00a79002e0cb9d6895ecd0919c506402850177d7b6c4d2634fa2da362d95bcb" ], "markers": "python_version >= '3.8'", - "version": "==1.34.93" + "version": "==1.34.94" }, "botocore-stubs": { "hashes": [ - "sha256:22e71e4c507df2e91d5673d2b65281c115d4c8d2601a3c2497c7a309fef6a0ec", - "sha256:909ebaf7a70ce83de4d85eed4b810e68c4d4cb64606d7431a263b909c92b7fa4" + "sha256:64d80a3467e3b19939e9c2750af33328b3087f8f524998dbdf7ed168227f507d", + "sha256:b0345f55babd8b901c53804fc5c326a4a0bd2e23e3b71f9ea5d9f7663466e6ba" ], "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==1.34.93" + "version": "==1.34.94" }, "click": { "hashes": [ @@ -1347,14 +1338,6 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "markers": "platform_system == 'Windows'", - "version": "==0.4.6" - }, "django": { "hashes": [ "sha256:a2d4c4d4ea0b6f0895acde632071aff6400bfc331228fc978b05452a0ff3e9f1", @@ -1382,20 +1365,20 @@ }, "django-stubs": { "hashes": [ - "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8", - "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b" + "sha256:084484cbe16a6d388e80ec687e46f529d67a232f3befaf55c936b3b476be289d", + "sha256:b8a792bee526d6cab31e197cb414ee7fa218abd931a50948c66a80b3a2548621" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.7" + "version": "==5.0.0" }, "django-stubs-ext": { "hashes": [ - "sha256:45a5d102417a412e3606e3c358adb4744988a92b7b58ccf3fd64bddd5d04d14c", - "sha256:519342ac0849cda1559746c9a563f03ff99f636b0ebe7c14b75e816a00dfddc3" + "sha256:5bacfbb498a206d5938454222b843d81da79ea8b6fcd1a59003f529e775bc115", + "sha256:8e1334fdf0c8bff87e25d593b33d4247487338aaed943037826244ff788b56a8" ], "markers": "python_version >= '3.8'", - "version": "==4.2.7" + "version": "==5.0.0" }, "django-webtest": { "hashes": [ @@ -1665,6 +1648,14 @@ "markers": "python_version >= '3.8'", "version": "==5.2.0" }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, "types-awscrt": { "hashes": [ "sha256:3ae374b553e7228ba41a528cf42bd0b2ad7303d806c73eff4aaaac1515e3ea4e", @@ -1682,14 +1673,6 @@ "markers": "python_version >= '3.7'", "version": "==5.3.0.7" }, - "types-pytz": { - "hashes": [ - "sha256:6810c8a1f68f21fdf0f4f374a432487c77645a0ac0b31de4bf4690cf21ad3981", - "sha256:8335d443310e2db7b74e007414e74c4f53b67452c0cb0d228ca359ccfba59659" - ], - "markers": "python_version >= '3.8'", - "version": "==2024.1.0.20240417" - }, "types-pyyaml": { "hashes": [ "sha256:a9e0f0f88dc835739b0c1ca51ee90d04ca2a897a71af79de9aec5f38cb0a5342", @@ -1724,14 +1707,6 @@ "markers": "python_version >= '3.8'", "version": "==4.11.0" }, - "tzdata": { - "hashes": [ - "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", - "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" - ], - "markers": "sys_platform == 'win32'", - "version": "==2024.1" - }, "urllib3": { "hashes": [ "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", diff --git a/src/registrar/admin.py b/src/registrar/admin.py index d06714de2..5eb7a0981 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2159,12 +2159,15 @@ class UserGroupAdmin(AuditedAdmin): def user_group(self, obj): return obj.name + class WaffleFlagAdmin(FlagAdmin): class Meta: """Contains meta information about this class""" + model = models.WaffleFlag fields = "__all__" + admin.site.unregister(LogEntry) # Unregister the default registration # Unregister samples and switches from django-waffle, as we currently don't use these. @@ -2195,4 +2198,4 @@ admin.site.register(models.TransitionDomain, TransitionDomainAdmin) admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) # Register our custom waffle flag implementation -admin.site.register(models.WaffleFlag, WaffleFlagAdmin) \ No newline at end of file +admin.site.register(models.WaffleFlag, WaffleFlagAdmin) diff --git a/src/registrar/migrations/0090_waffleflag.py b/src/registrar/migrations/0090_waffleflag.py index ff449d77a..08ea178c6 100644 --- a/src/registrar/migrations/0090_waffleflag.py +++ b/src/registrar/migrations/0090_waffleflag.py @@ -8,33 +8,120 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ('registrar', '0089_user_verification_type copy'), + ("auth", "0012_alter_user_first_name_max_length"), + ("registrar", "0089_user_verification_type copy"), ] operations = [ migrations.CreateModel( - name='WaffleFlag', + name="WaffleFlag", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text='The human/computer readable name.', max_length=100, unique=True, verbose_name='Name')), - ('everyone', models.BooleanField(blank=True, help_text='Flip this flag on (Yes) or off (No) for everyone, overriding all other settings. Leave as Unknown to use normally.', null=True, verbose_name='Everyone')), - ('percent', models.DecimalField(blank=True, decimal_places=1, help_text='A number between 0.0 and 99.9 to indicate a percentage of users for whom this flag will be active.', max_digits=3, null=True, verbose_name='Percent')), - ('testing', models.BooleanField(default=False, help_text='Allow this flag to be set for a session for user testing', verbose_name='Testing')), - ('superusers', models.BooleanField(default=True, help_text='Flag always active for superusers?', verbose_name='Superusers')), - ('staff', models.BooleanField(default=False, help_text='Flag always active for staff?', verbose_name='Staff')), - ('authenticated', models.BooleanField(default=False, help_text='Flag always active for authenticated users?', verbose_name='Authenticated')), - ('languages', models.TextField(blank=True, default='', help_text='Activate this flag for users with one of these languages (comma-separated list)', verbose_name='Languages')), - ('rollout', models.BooleanField(default=False, help_text='Activate roll-out mode?', verbose_name='Rollout')), - ('note', models.TextField(blank=True, help_text='Note where this Flag is used.', verbose_name='Note')), - ('created', models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='Date when this Flag was created.', verbose_name='Created')), - ('modified', models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this Flag was last modified.', verbose_name='Modified')), - ('groups', models.ManyToManyField(blank=True, help_text='Activate this flag for these user groups.', to='auth.group', verbose_name='Groups')), - ('users', models.ManyToManyField(blank=True, help_text='Activate this flag for these users.', to=settings.AUTH_USER_MODEL, verbose_name='Users')), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "name", + models.CharField( + help_text="The human/computer readable name.", max_length=100, unique=True, verbose_name="Name" + ), + ), + ( + "everyone", + models.BooleanField( + blank=True, + help_text="Flip this flag on (Yes) or off (No) for everyone, overriding all other settings. Leave as Unknown to use normally.", + null=True, + verbose_name="Everyone", + ), + ), + ( + "percent", + models.DecimalField( + blank=True, + decimal_places=1, + help_text="A number between 0.0 and 99.9 to indicate a percentage of users for whom this flag will be active.", + max_digits=3, + null=True, + verbose_name="Percent", + ), + ), + ( + "testing", + models.BooleanField( + default=False, + help_text="Allow this flag to be set for a session for user testing", + verbose_name="Testing", + ), + ), + ( + "superusers", + models.BooleanField( + default=True, help_text="Flag always active for superusers?", verbose_name="Superusers" + ), + ), + ( + "staff", + models.BooleanField(default=False, help_text="Flag always active for staff?", verbose_name="Staff"), + ), + ( + "authenticated", + models.BooleanField( + default=False, + help_text="Flag always active for authenticated users?", + verbose_name="Authenticated", + ), + ), + ( + "languages", + models.TextField( + blank=True, + default="", + help_text="Activate this flag for users with one of these languages (comma-separated list)", + verbose_name="Languages", + ), + ), + ( + "rollout", + models.BooleanField(default=False, help_text="Activate roll-out mode?", verbose_name="Rollout"), + ), + ("note", models.TextField(blank=True, help_text="Note where this Flag is used.", verbose_name="Note")), + ( + "created", + models.DateTimeField( + db_index=True, + default=django.utils.timezone.now, + help_text="Date when this Flag was created.", + verbose_name="Created", + ), + ), + ( + "modified", + models.DateTimeField( + default=django.utils.timezone.now, + help_text="Date when this Flag was last modified.", + verbose_name="Modified", + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="Activate this flag for these user groups.", + to="auth.group", + verbose_name="Groups", + ), + ), + ( + "users", + models.ManyToManyField( + blank=True, + help_text="Activate this flag for these users.", + to=settings.AUTH_USER_MODEL, + verbose_name="Users", + ), + ), ], options={ - 'verbose_name': 'waffle flag', - 'verbose_name_plural': 'Waffle flags', + "verbose_name": "waffle flag", + "verbose_name_plural": "Waffle flags", }, ), ] diff --git a/src/registrar/migrations/0091_create_waffle_flags_v01.py b/src/registrar/migrations/0091_create_waffle_flags_v01.py index c98542a14..c643eb12c 100644 --- a/src/registrar/migrations/0091_create_waffle_flags_v01.py +++ b/src/registrar/migrations/0091_create_waffle_flags_v01.py @@ -9,19 +9,25 @@ from typing import Any # For linting: RunPython expects a function reference, # so let's give it one -def create_flags(): +def create_flags(apps, schema_editor): """ Populates pre-existing flags we wish to associate. Only generates a flag name and a note, but no other data is loaded at this point. """ - WaffleFlag.create_waffle_flags_for_migrations() -def delete_flags(): + # This is a bit of a hack to get around "apps" not knowing what the concept of a constant is + default_flags = WaffleFlag.get_default_waffle_flags() + WaffleFlag.create_waffle_flags_for_migrations(apps, default_flags) + + +def delete_flags(apps, schema_editor): """ - Deletes all prexisting flags. + Deletes all prexisting flags. Does not delete flags not defined in this scope (user generated). """ - WaffleFlag.delete_waffle_flags_for_migrations() + # This is a bit of a hack to get around "apps" not knowing what the concept of a constant is + default_flags = WaffleFlag.get_default_waffle_flags() + WaffleFlag.delete_waffle_flags_for_migrations(apps, default_flags) class Migration(migrations.Migration): diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index aac9e09b6..14ae25df4 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -53,4 +53,4 @@ auditlog.register(UserGroup, m2m_fields=["permissions"]) auditlog.register(Website) auditlog.register(TransitionDomain) auditlog.register(VerifiedByStaff) -auditlog.register(WaffleFlag) \ No newline at end of file +auditlog.register(WaffleFlag) diff --git a/src/registrar/models/waffle_flag.py b/src/registrar/models/waffle_flag.py index 8fe710dbd..ee6199677 100644 --- a/src/registrar/models/waffle_flag.py +++ b/src/registrar/models/waffle_flag.py @@ -14,48 +14,56 @@ class WaffleFlag(AbstractUserFlag): class Meta: """Contains meta information about this class""" + verbose_name = "waffle flag" verbose_name_plural = "Waffle flags" - # Defines which waffle flags should be created at startup. - # Add to this list if you want to add another flag that is generated at startup. - # When you do so, you will need to add a new instance of `0091_create_waffle_flags_v{version_number}` - # in registrar/migrations for that change to update automatically on migrate. - DEFAULT_WAFFLE_FLAGS = [ - "profile_feature", - "dns_hosting_feature" - ] + @staticmethod + def get_default_waffle_flags(): + """ + Defines which waffle flags should be created at startup. + Add to this list if you want to add another flag that is generated at startup. + When you do so, you will need to add a new instance of `0091_create_waffle_flags_v{version_number}` + in registrar/migrations for that change to update automatically on migrate. + This has to exist here, as from the context of migrations, it cannot access constants + """ + default_flags = [ + # flag_name, flag_note + ("profile_feature", "Used for profiles"), + ("dns_hosting_feature", "Used for dns hosting"), + ] + return default_flags - @classmethod - def create_waffle_flags_for_migrations(cls): + @staticmethod + def create_waffle_flags_for_migrations(apps, default_waffle_flags): """ Creates a pre-defined list of flags for our migrations. """ logger.info("Creating default waffle flags...") + WaffleFlag = apps.get_model("registrar", "WaffleFlag") # Flags can be changed through the command line or through django admin. # To keep the scope of this function minimal and simple, if we require additional # config on these flag, it should be done in a seperate function or as a command. - for flag_name in cls.DEFAULT_WAFFLE_FLAGS: + for flag_name, flag_note in default_waffle_flags: try: - cls.objects.update_or_create( + WaffleFlag.objects.update_or_create( name=flag_name, # Booleans like superusers or is_staff can be set here, if needed. - defaults={ - 'note': 'Auto-generated waffle flag' - } + defaults={"note": flag_note}, ) except Exception as e: logger.error(f"An error occurred when attempting to add or update flag {flag_name}: {e}") - - @classmethod - def delete_waffle_flags_for_migrations(cls): + + @staticmethod + def delete_waffle_flags_for_migrations(apps, default_waffle_flags): """ Delete a pre-defined list of flags for our migrations (the reverse_code operation). """ logger.info("Deleting default waffle flags...") - existing_flags = cls.objects.filter(name__in=cls.DEFAULT_WAFFLE_FLAGS) + WaffleFlag = apps.get_model("registrar", "WaffleFlag") + existing_flags = WaffleFlag.objects.filter(name__in=default_waffle_flags) for flag in existing_flags: try: - cls.objects.get(name=flag.name).delete() + WaffleFlag.objects.get(name=flag.name).delete() except Exception as e: - logger.error(f"An error occurred when attempting to delete flag {flag.name}: {e}") \ No newline at end of file + logger.error(f"An error occurred when attempting to delete flag {flag.name}: {e}") diff --git a/src/requirements.txt b/src/requirements.txt index 0a79d1f59a6b0ee6aef7ba94f297f8d81e9e5752..849721a1d1264b53b3c48d6f6fe0983062ce8974 100644 GIT binary patch literal 2900 zcmbVOO>^5e5WVYHs3#p-Li`eCHN#APLhhMP1|mQb76>2&Xqi6rxAy_G9M>+GCKrwb zE*JaW?!MhbYMfr?Q~Js87WF~D%b5AJ@Wry+R&!wPHTxM52PSP z#OralY53?wE9yE=^OEEwkC;|4ZMk~zu;CJa{6`td77iqJtsaN3&twF~2Cl%D=j1nV z@_GI|!$(g?%Uz;iOr?0@qvxl1b!|II`G(9RN%7@48>hRGntrb9GAD>55-VUa-cFXY z$3gmlj}iWJa@=jHVsCSZL~n)ic})M<79{hJ+&>EcDB=D-VbswxwLFw)vr2d!> zRLJEFC#1p|Rh>q}AU9hnm&GojY!E6LCg}u4v6G%^MjJJNzW-meR~w3BjO7+{|utBCY1f{ ze`^OH2HN0ug1`WC=!eFH=o#{V%c5MW>r+{}zB=lO$+HI8qF$ufwMzPneW>)i_QVvdpLz%GOY zSl2JOzh1Zvi9y2vykA57u;G&vVbDMwiP6@*PqXLt=Q;2UFPCT+2K-tlgnT>+W0pZw zkSda43`wuGn6Z9%MA(93NH#ndKdhx|kMXFq hun?MReOGuO zN&1va@O}6$+-Y>J(Morfp5>vVD>4er{Z&U#Z%=gg!Y^76`7ZTMc&k~vD3evL!f%>? z6<&p*=HKXQE0kJ!p?O7^MTs`r4gHo9r%~UHRzbhM)*6P9es6Vcr=v^EB2L8@lbZ?E zOg<=t*+$25kLft-jNWPbUhgr@BJPz?o#_OHf&4RwC=4WfkSKR@kUNv)wfyRyH%;+l zrnP)EFz+9+Vj1}}i}1oai0*dM>Te37hipPSz}=WMra6N<5(pa%+q~9od#6t|H1F z;l1#q-Uq$@;cjra8y@aPdvx;fAW!5%1^-WVVo669?e>GOe`MsDkl_kW} zN_`tSp_F!HxVQS>l-AwLdaZTF%80pexDjt=x=wVAbVrAj>FnwEPMy+q9J|wV{&5}Y z-I>OzF-YlaCOL*SJJ{ zi$o&Iq*}BRwP`Ws`FAcn*YdUH(e++aP)bD>Oo!B(=^ z1$bmzWlNLkx-A=>3>m`ecA6~jU+M226(Oz{wx)W*GY+zLlJ+EPi99-E_W_jI+Soj5}#Bzx685B)~A=epGnuc~k& z6JF#fs;T!QbPB22)44(S{9z@nt0b0MT{ll#M}oiJ#|YX=i-B_QG-`xDtrv%5RPWnF zoNK+1kHxmbu>KCI10A17{MhNO*7w_D!8TVh|Y}3t+PKFy`OOtawmg^eEZv5IjzakU^}d z%{`d!lnP}a^sv1iI#}Py7oJU&lM-Fuf)Q37Pi^5FlXY=!cblr=`3dPSLWQ2n`!(c| z2PTOT=*%V8(fyluYM&2dPEJADYOLWy-mz+w`u*?Qv8V@^`NW+&%O!dDr)xM@$9$^>oZXX9= zY%@@I-5Oy8y0wtuVvyp`-;Q Date: Tue, 30 Apr 2024 12:35:30 -0600 Subject: [PATCH 29/76] Fix fixtures --- src/registrar/fixtures_users.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index 7f991fa0e..ee1ca7e81 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -196,12 +196,12 @@ class UserFixture: }, ] - def load_users(cls, users, group_name): + def load_users(cls, users, group_name, set_users_superusers=False): logger.info(f"Going to load {len(users)} users in group {group_name}") for user_data in users: try: user, _ = User.objects.get_or_create(username=user_data["username"]) - user.is_superuser = False + user.is_superuser = set_users_superusers user.first_name = user_data["first_name"] user.last_name = user_data["last_name"] if "email" in user_data: @@ -229,5 +229,5 @@ class UserFixture: # steps now do not need to close/reopen a db connection, # instead they share one. with transaction.atomic(): - cls.load_users(cls, cls.ADMINS, "full_access_group") + cls.load_users(cls, cls.ADMINS, "full_access_group", set_users_superusers=True) cls.load_users(cls, cls.STAFF, "cisa_analysts_group") From 8dc48d21f49bf006ee65889fa693b27b74c05d6a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:07:26 -0600 Subject: [PATCH 30/76] Add documentation --- docs/developer/adding-feature-flags.md | 30 ++++++++++++++++++++++++++ docs/developer/feature-flags.md | 0 src/registrar/admin.py | 12 ++++++----- src/registrar/fixtures_users.py | 6 +++--- 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 docs/developer/adding-feature-flags.md delete mode 100644 docs/developer/feature-flags.md diff --git a/docs/developer/adding-feature-flags.md b/docs/developer/adding-feature-flags.md new file mode 100644 index 000000000..711b6b7b6 --- /dev/null +++ b/docs/developer/adding-feature-flags.md @@ -0,0 +1,30 @@ +# Adding feature flags +Feature flags are booleans (stored in our DB as the `WaffleFlag` object) that programmatically disable/enable "features" (such as DNS hosting) for a specified set of users. + +We use [django-waffle](https://waffle.readthedocs.io/en/stable/) for our feature flags. Waffle makes using flags fairly straight forward. + +## Adding feature flags through django admin +1. On the app, navigate to `\admin`. +2. Under models, click `Waffle flags`. +3. Click `Add waffle flag`. +4. Add the model as you would normally. Refer to waffle's documentation [regarding attributes](https://waffle.readthedocs.io/en/stable/types/flag.html#flag-attributes) for more information on them. + +## Adding feature flags when migrations are ran +Given that we store waffle flags as a predefined list, this means that we need to create a new migration file when we want to add a set of feature flags programatically this way. Note that if `WAFFLE_CREATE_MISSING_FLAGS` is set to True, you may not need this step. + +Follow these steps to achieve this: +1. Navigate to `registrar/models/waffle_flag.py`. +2. Modify the `get_default_waffle_flags` and add the desired name of your feature flag to the `default_flags` array. +3. Navigate to `registrar/migrationdata`. +4. Copy the migration named `0091_create_waffle_flags_v01`. +5. Rename the copied migration to match the increment. For instance, if `0091_create_waffle_flags_v01` exists, you will rename your migration to `0091_create_waffle_flags_v02`. +6. Modify the migration dependency to match the last migration in the stack. + +## Modifying an existing feature flag through the CLI +Waffle comes with built in management commands that you can use to update records remotely. [Read here](https://waffle.readthedocs.io/en/stable/usage/cli.html) for information on how to use them. + +## Using feature flags as boolean values +Waffle [provides a boolean](https://waffle.readthedocs.io/en/stable/usage/views.html) called `flag_is_active` that you can use as you otherwise would a boolean. This boolean requires a request object and the flag name. + +## Using feature flags to disable/enable views +Waffle [provides a decorator](https://waffle.readthedocs.io/en/stable/usage/decorators.html) that you can use to enable/disable views. When disabled, the view will return a 404 if said user tries to navigate to it. diff --git a/docs/developer/feature-flags.md b/docs/developer/feature-flags.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 5eb7a0981..bd6666c8b 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2170,11 +2170,6 @@ class WaffleFlagAdmin(FlagAdmin): admin.site.unregister(LogEntry) # Unregister the default registration -# Unregister samples and switches from django-waffle, as we currently don't use these. -# TODO - address this -admin.site.unregister(Sample) -admin.site.unregister(Switch) - admin.site.register(LogEntry, CustomLogEntryAdmin) admin.site.register(models.User, MyUserAdmin) # Unregister the built-in Group model @@ -2199,3 +2194,10 @@ admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) # Register our custom waffle flag implementation admin.site.register(models.WaffleFlag, WaffleFlagAdmin) + +# Unregister samples and switches from django-waffle, as we currently don't use these. +# Django admin sorts different "sites" alphabetically, and offers little customization for them. +# If we do need to use these, we should also consider using this library: +# https://pypi.org/project/django-reorder-admin/ +admin.site.unregister(Sample) +admin.site.unregister(Switch) diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index ee1ca7e81..c31acacfd 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -196,12 +196,12 @@ class UserFixture: }, ] - def load_users(cls, users, group_name, set_users_superusers=False): + def load_users(cls, users, group_name, are_superusers=False): logger.info(f"Going to load {len(users)} users in group {group_name}") for user_data in users: try: user, _ = User.objects.get_or_create(username=user_data["username"]) - user.is_superuser = set_users_superusers + user.is_superuser = are_superusers user.first_name = user_data["first_name"] user.last_name = user_data["last_name"] if "email" in user_data: @@ -229,5 +229,5 @@ class UserFixture: # steps now do not need to close/reopen a db connection, # instead they share one. with transaction.atomic(): - cls.load_users(cls, cls.ADMINS, "full_access_group", set_users_superusers=True) + cls.load_users(cls, cls.ADMINS, "full_access_group", are_superusers=True) cls.load_users(cls, cls.STAFF, "cisa_analysts_group") From f6c154c94b04585b9c7ae03990cff767c8b0d3c3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:58:46 -0600 Subject: [PATCH 31/76] Refactor tests --- src/registrar/tests/common.py | 17 ++++- src/registrar/tests/test_admin.py | 16 ++-- src/registrar/tests/test_feature_flags.py | 89 +++++++++++++++++++++++ src/registrar/tests/test_views_domain.py | 4 +- src/registrar/views/index.py | 1 + 5 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 src/registrar/tests/test_feature_flags.py diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 6dd88c1c1..b4c00b725 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -753,12 +753,14 @@ def mock_user(): def create_superuser(): + """Creates a user with admin-level permissions""" User = get_user_model() p = "adminpass" user = User.objects.create_user( username="superuser", email="admin@example.com", is_staff=True, + is_superuser=True, password=p, ) # Retrieve the group or create it if it doesn't exist @@ -768,7 +770,8 @@ def create_superuser(): return user -def create_user(): +def create_staffuser(): + """Creates a user with staff level permissions""" User = get_user_model() p = "userpass" user = User.objects.create_user( @@ -783,6 +786,18 @@ def create_user(): user.groups.set([group]) return user +def create_user(): + """Creates a user with no special permissions""" + User = get_user_model() + p = "userpass" + user = User.objects.create_user( + username="regularuser", + email="regularuser@example.com", + is_staff=False, + is_superuser=False, + password=p, + ) + return user def create_ready_domain(): domain, _ = Domain.objects.get_or_create(name="city.gov", state=Domain.State.READY) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index b42667e01..9250b1f2b 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -44,7 +44,7 @@ from .common import ( less_console_noise, mock_user, create_superuser, - create_user, + create_staffuser, create_ready_domain, multiple_unalphabetical_domain_objects, MockEppLib, @@ -72,7 +72,7 @@ class TestDomainAdmin(MockEppLib, WebTest): self.admin = DomainAdmin(model=Domain, admin_site=self.site) self.client = Client(HTTP_HOST="localhost:8080") self.superuser = create_superuser() - self.staffuser = create_user() + self.staffuser = create_staffuser() self.factory = RequestFactory() self.app.set_user(self.superuser.username) self.client.force_login(self.superuser) @@ -841,7 +841,7 @@ class TestDomainRequestAdmin(MockEppLib): self.factory = RequestFactory() self.admin = DomainRequestAdmin(model=DomainRequest, admin_site=self.site) self.superuser = create_superuser() - self.staffuser = create_user() + self.staffuser = create_staffuser() self.client = Client(HTTP_HOST="localhost:8080") self.test_helper = GenericTestHelper( factory=self.factory, @@ -2502,7 +2502,7 @@ class TestDomainInformationAdmin(TestCase): self.admin = DomainInformationAdmin(model=DomainInformation, admin_site=self.site) self.client = Client(HTTP_HOST="localhost:8080") self.superuser = create_superuser() - self.staffuser = create_user() + self.staffuser = create_staffuser() self.mock_data_generator = AuditedAdminMockData() self.test_helper = GenericTestHelper( @@ -3012,7 +3012,7 @@ class TestMyUserAdmin(TestCase): """ Tests for the correct helper text on this page """ - user = create_user() + user = create_staffuser() p = "adminpass" self.client.login(username="superuser", password=p) @@ -3036,7 +3036,7 @@ class TestMyUserAdmin(TestCase): def test_list_display_without_username(self): with less_console_noise(): request = self.client.request().wsgi_request - request.user = create_user() + request.user = create_staffuser() list_display = self.admin.get_list_display(request) expected_list_display = [ @@ -3062,7 +3062,7 @@ class TestMyUserAdmin(TestCase): def test_get_fieldsets_cisa_analyst(self): with less_console_noise(): request = self.client.request().wsgi_request - request.user = create_user() + request.user = create_staffuser() fieldsets = self.admin.get_fieldsets(request) expected_fieldsets = ( ( @@ -3448,7 +3448,7 @@ class ContactAdminTest(TestCase): self.client = Client(HTTP_HOST="localhost:8080") self.admin = ContactAdmin(model=get_user_model(), admin_site=None) self.superuser = create_superuser() - self.staffuser = create_user() + self.staffuser = create_staffuser() def test_readonly_when_restricted_staffuser(self): with less_console_noise(): diff --git a/src/registrar/tests/test_feature_flags.py b/src/registrar/tests/test_feature_flags.py new file mode 100644 index 000000000..ef0a1daf5 --- /dev/null +++ b/src/registrar/tests/test_feature_flags.py @@ -0,0 +1,89 @@ +from unittest import skip +from waffle.decorators import flag_is_active +from django.test import TestCase, Client, RequestFactory +from registrar.models import ( + WaffleFlag, + User, + Contact +) +from registrar.tests.common import create_superuser, create_staffuser, create_user + +class TestFeatureFlags(TestCase): + def setUp(self): + super().setUp() + self.client = Client(HTTP_HOST="localhost:8080") + self.factory = RequestFactory() + self.superuser = create_superuser() + self.staffuser = create_staffuser() + self.user = create_user() + + def tearDown(self): + super().tearDown() + WaffleFlag.objects.all().delete() + User.objects.all().delete() + Contact.objects.all().delete() + + def test_flag_active_for_superuser(self): + """ + Tests flag_is_active for a flag with `superuser = True` + """ + flag, _ = WaffleFlag.objects.get_or_create( + name="test_superuser_flag", + superusers=True, + staff=False, + ) + # Test if superusers can access this flag + request = self.factory.get("/") + request.user = self.superuser + self.assertTrue(flag_is_active(request, flag.name)) + + # Ensure that regular staff cannot access this flag + request_staff = self.factory.get("/") + request_staff.user = self.staffuser + self.assertFalse(flag_is_active(request_staff, flag.name)) + + # Ensure that a normal user also can't access this flag + request_normal = self.factory.get("/") + request_normal.user = self.user + self.assertFalse(flag_is_active(request_normal, flag.name)) + + @skip("not implemented yet") + def test_flag_active_for_is_staff(self): + """ + Tests flag_is_active for a flag with `is_staff = True` + """ + # Test if staff can access this flag + # Ensure that superusers cannot + raise + + @skip("not implemented yet") + def test_flag_active_for_everyone(self): + """ + Tests flag_is_active for a flag with `everyone = True` + """ + # Test if superuser, analyst, and a normal user can access + raise + + @skip("not implemented yet") + def test_flag_active_for_everyone_is_false(self): + """ + Tests flag_is_active for a flag with `everyone = False` + """ + # Test if no user type can access + raise + + @skip("not implemented yet") + def test_admin_group(self): + """ + Tests flag_is_active for the admin user group + """ + # Test if no user type can access + raise + + @skip("not implemented yet") + def test_staff_group(self): + """ + Tests flag_is_active for the staff user group + """ + # Test if no user type can access + raise \ No newline at end of file diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index fc391f8b5..7bd6a0f18 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -5,7 +5,7 @@ from django.conf import settings from django.urls import reverse from django.contrib.auth import get_user_model -from .common import MockEppLib, MockSESClient, create_user # type: ignore +from .common import MockEppLib, MockSESClient, create_staffuser # type: ignore from django_webtest import WebTest # type: ignore import boto3_mocking # type: ignore @@ -322,7 +322,7 @@ class TestDomainDetail(TestDomainOverview): when no domain information or domain request exist""" with less_console_noise(): # have to use staff user for this test - staff_user = create_user() + staff_user = create_staffuser() # staff_user.save() self.client.force_login(staff_user) diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py index cb5fb79fa..a0499f7f9 100644 --- a/src/registrar/views/index.py +++ b/src/registrar/views/index.py @@ -3,6 +3,7 @@ from django.shortcuts import render from registrar.models import DomainRequest, Domain, UserDomainRole from waffle.decorators import flag_is_active + def index(request): """This page is available to anyone without logging in.""" context = {} From cb68c74b6897e021453d507c81d273896b9cacac Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 1 May 2024 08:18:02 -0600 Subject: [PATCH 32/76] Update piplock --- src/Pipfile.lock | 190 +++++++++++++++++++++---------------------- src/requirements.txt | 9 +- 2 files changed, 100 insertions(+), 99 deletions(-) diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 117290daa..d8fb1afee 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "16a0db98015509322cf1d27f06fced5b7635057c4eb98921a9419d63d51925ab" + "sha256": "9095c4f98f58a9502444584067a63f329d5a5fc4b49454c4e129bda09552d19d" }, "pipfile-spec": 6, "requires": {}, @@ -32,20 +32,20 @@ }, "boto3": { "hashes": [ - "sha256:2824e3dd18743ca50e5b10439d20e74647b1416e8a94509cb30beac92d27a18d", - "sha256:b2e5cb5b95efcc881e25a3bc872d7a24e75ff4e76f368138e4baf7b9d6ee3422" + "sha256:decf52f8d5d8a1b10c9ff2a0e96ee207ed79e33d2e53fdf0880a5cbef70785e0", + "sha256:e836b71d79671270fccac0a4d4c8ec239a6b82ea47c399b64675aa597d0ee63b" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.90" + "version": "==1.34.95" }, "botocore": { "hashes": [ - "sha256:113cd4c0cb63e13163ccbc2bb13d551be314ba7f8ba5bfab1c51a19ca01aa133", - "sha256:d48f152498e2c60b43ce25b579d26642346a327b6fb2c632d57219e0a4f63392" + "sha256:6bd76a2eadb42b91fa3528392e981ad5b4dfdee3968fa5b904278acf6cbf15ff", + "sha256:ead5823e0dd6751ece5498cb979fd9abf190e691c8833bcac6876fd6ca261fa7" ], "markers": "python_version >= '3.8'", - "version": "==1.34.90" + "version": "==1.34.95" }, "cachetools": { "hashes": [ @@ -370,6 +370,15 @@ "markers": "python_version >= '3.8'", "version": "==7.3.0" }, + "django-waffle": { + "hashes": [ + "sha256:5979a2f3dd674ef7086480525b39651fc2045427f6d8e6a614192656d3402c5b", + "sha256:e49d7d461d89f3bd8e53f20efe39310acca8f275c9888495e68e195345bf18b1" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.1.0" + }, "django-widget-tweaks": { "hashes": [ "sha256:1c2180681ebb994e922c754804c7ffebbe1245014777ac47897a81f57cc629c7", @@ -392,12 +401,12 @@ }, "faker": { "hashes": [ - "sha256:34b947581c2bced340c39b35f89dbfac4f356932cfff8fe893bde854903f0e6e", - "sha256:adb98e771073a06bdc5d2d6710d8af07ac5da64c8dc2ae3b17bb32319e66fd82" + "sha256:87ef41e24b39a5be66ecd874af86f77eebd26782a2681200e86c5326340a95d3", + "sha256:e23a2b74888885c3d23a9237bacb823041291c03d609a39acb9ebe6c123f3986" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==24.11.0" + "version": "==25.0.0" }, "fred-epplib": { "git": "https://github.com/cisagov/epplib.git", @@ -588,7 +597,6 @@ "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b", "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6", "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8", - "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5", "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306", "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5", "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f", @@ -801,12 +809,12 @@ }, "oic": { "hashes": [ - "sha256:385a1f64bb59519df1e23840530921bf416740240f505ea6d161e331d3d39fad", - "sha256:fcbf948a22e4d4df66f6bf57d327933f32a7b539640d9b42883457634360ba78" + "sha256:b74bd06c7de1ab4f8e798f714062e6a68f68ad9cdbed1f1c30a7fb887602f321", + "sha256:e51705d0c14c97e9ca594374bfb54269a72c9b489e0e979598344c0189bfcb64" ], "index": "pypi", - "markers": "python_version ~= '3.7'", - "version": "==1.6.1" + "markers": "python_version ~= '3.8'", + "version": "==1.7.0" }, "orderedmultidict": { "hashes": [ @@ -1244,49 +1252,49 @@ }, "black": { "hashes": [ - "sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d", - "sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd", - "sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33", - "sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965", - "sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070", - "sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397", - "sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745", - "sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1", - "sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665", - "sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436", - "sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb", - "sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e", - "sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6", - "sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702", - "sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8", - "sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8", - "sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3", - "sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad", - "sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf", - "sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e", - "sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641", - "sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2" + "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474", + "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1", + "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0", + "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8", + "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96", + "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1", + "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04", + "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021", + "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94", + "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d", + "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c", + "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7", + "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c", + "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc", + "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7", + "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d", + "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c", + "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741", + "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce", + "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb", + "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063", + "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==24.4.0" + "version": "==24.4.2" }, "blinker": { "hashes": [ - "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9", - "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182" + "sha256:5f1cdeff423b77c31b89de0565cd03e5275a03028f44b2b15f912632a58cced6", + "sha256:da44ec748222dcd0105ef975eed946da197d5bdf8bafb6aa92f5bc89da63fa25" ], "markers": "python_version >= '3.8'", - "version": "==1.7.0" + "version": "==1.8.1" }, "boto3": { "hashes": [ - "sha256:2824e3dd18743ca50e5b10439d20e74647b1416e8a94509cb30beac92d27a18d", - "sha256:b2e5cb5b95efcc881e25a3bc872d7a24e75ff4e76f368138e4baf7b9d6ee3422" + "sha256:decf52f8d5d8a1b10c9ff2a0e96ee207ed79e33d2e53fdf0880a5cbef70785e0", + "sha256:e836b71d79671270fccac0a4d4c8ec239a6b82ea47c399b64675aa597d0ee63b" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.90" + "version": "==1.34.95" }, "boto3-mocking": { "hashes": [ @@ -1299,28 +1307,28 @@ }, "boto3-stubs": { "hashes": [ - "sha256:7361f162523168ddcfb3e0cc70e5208e78f95b9f1f2553032036a2b67ab33355", - "sha256:c82f3db8558e28f766361ba1eea7c77dff735f72fef2a0b9dffaa9c0d9ae76a3" + "sha256:412006b27ee707e9b51a084b02ac92b143af8a3b56727582afec2a76ce93c3b6", + "sha256:4fb5830626de42446c238ca72ca1a53e461281396007fb900edf50ceeb044a10" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.90" + "version": "==1.34.95" }, "botocore": { "hashes": [ - "sha256:113cd4c0cb63e13163ccbc2bb13d551be314ba7f8ba5bfab1c51a19ca01aa133", - "sha256:d48f152498e2c60b43ce25b579d26642346a327b6fb2c632d57219e0a4f63392" + "sha256:6bd76a2eadb42b91fa3528392e981ad5b4dfdee3968fa5b904278acf6cbf15ff", + "sha256:ead5823e0dd6751ece5498cb979fd9abf190e691c8833bcac6876fd6ca261fa7" ], "markers": "python_version >= '3.8'", - "version": "==1.34.90" + "version": "==1.34.95" }, "botocore-stubs": { "hashes": [ - "sha256:b2d7416b524bce7325aa5fe09bb5e0b6bc9531d4136f4407fa39b6bc58507f34", - "sha256:d9b66542cbb8fbe28eef3c22caf941ae22d36cc1ef55b93fc0b52239457cab57" + "sha256:64d80a3467e3b19939e9c2750af33328b3087f8f524998dbdf7ed168227f507d", + "sha256:b0345f55babd8b901c53804fc5c326a4a0bd2e23e3b71f9ea5d9f7663466e6ba" ], "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==1.34.89" + "version": "==1.34.94" }, "click": { "hashes": [ @@ -1357,20 +1365,20 @@ }, "django-stubs": { "hashes": [ - "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8", - "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b" + "sha256:084484cbe16a6d388e80ec687e46f529d67a232f3befaf55c936b3b476be289d", + "sha256:b8a792bee526d6cab31e197cb414ee7fa218abd931a50948c66a80b3a2548621" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.7" + "version": "==5.0.0" }, "django-stubs-ext": { "hashes": [ - "sha256:45a5d102417a412e3606e3c358adb4744988a92b7b58ccf3fd64bddd5d04d14c", - "sha256:519342ac0849cda1559746c9a563f03ff99f636b0ebe7c14b75e816a00dfddc3" + "sha256:5bacfbb498a206d5938454222b843d81da79ea8b6fcd1a59003f529e775bc115", + "sha256:8e1334fdf0c8bff87e25d593b33d4247487338aaed943037826244ff788b56a8" ], "markers": "python_version >= '3.8'", - "version": "==4.2.7" + "version": "==5.0.0" }, "django-webtest": { "hashes": [ @@ -1423,37 +1431,37 @@ }, "mypy": { "hashes": [ - "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6", - "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913", - "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129", - "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc", - "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974", - "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374", - "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150", - "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03", - "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9", - "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02", - "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89", - "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2", - "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d", - "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3", - "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612", - "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e", - "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3", - "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e", - "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd", - "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04", - "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed", - "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185", - "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf", - "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b", - "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4", - "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f", - "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6" + "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061", + "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99", + "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de", + "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a", + "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9", + "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec", + "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1", + "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131", + "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f", + "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821", + "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5", + "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee", + "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e", + "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746", + "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2", + "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0", + "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b", + "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53", + "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30", + "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda", + "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051", + "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2", + "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7", + "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee", + "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727", + "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976", + "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.9.0" + "version": "==1.10.0" }, "mypy-extensions": { "hashes": [ @@ -1665,14 +1673,6 @@ "markers": "python_version >= '3.7'", "version": "==5.3.0.7" }, - "types-pytz": { - "hashes": [ - "sha256:6810c8a1f68f21fdf0f4f374a432487c77645a0ac0b31de4bf4690cf21ad3981", - "sha256:8335d443310e2db7b74e007414e74c4f53b67452c0cb0d228ca359ccfba59659" - ], - "markers": "python_version >= '3.8'", - "version": "==2024.1.0.20240417" - }, "types-pyyaml": { "hashes": [ "sha256:a9e0f0f88dc835739b0c1ca51ee90d04ca2a897a71af79de9aec5f38cb0a5342", diff --git a/src/requirements.txt b/src/requirements.txt index 017d42542..210f77f76 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,8 +1,8 @@ -i https://pypi.python.org/simple annotated-types==0.6.0; python_version >= '3.8' asgiref==3.8.1; python_version >= '3.8' -boto3==1.34.90; python_version >= '3.8' -botocore==1.34.90; python_version >= '3.8' +boto3==1.34.95; python_version >= '3.8' +botocore==1.34.95; python_version >= '3.8' cachetools==5.3.3; python_version >= '3.7' certifi==2024.2.2; python_version >= '3.6' cfenv==0.5.3 @@ -22,9 +22,10 @@ django-csp==3.8 django-fsm==2.8.1 django-login-required-middleware==0.9.0 django-phonenumber-field[phonenumberslite]==7.3.0; python_version >= '3.8' +django-waffle==4.1.0; python_version >= '3.8' django-widget-tweaks==1.5.0; python_version >= '3.8' environs[django]==11.0.0; python_version >= '3.8' -faker==24.11.0; python_version >= '3.8' +faker==25.0.0; python_version >= '3.8' fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c furl==2.1.3 future==1.0.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' @@ -37,7 +38,7 @@ lxml==5.2.1; python_version >= '3.6' mako==1.3.3; python_version >= '3.8' markupsafe==2.1.5; python_version >= '3.7' marshmallow==3.21.1; python_version >= '3.8' -oic==1.6.1; python_version ~= '3.7' +oic==1.7.0; python_version ~= '3.8' orderedmultidict==1.0.1 packaging==24.0; python_version >= '3.7' phonenumberslite==8.13.35 From b50f14fd3b57fcd3fd77090f0cba329174fbfaee Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 1 May 2024 08:40:55 -0600 Subject: [PATCH 33/76] Unit tests --- src/registrar/tests/test_feature_flags.py | 128 +++++++++++++++++----- 1 file changed, 100 insertions(+), 28 deletions(-) diff --git a/src/registrar/tests/test_feature_flags.py b/src/registrar/tests/test_feature_flags.py index ef0a1daf5..c24a07ee8 100644 --- a/src/registrar/tests/test_feature_flags.py +++ b/src/registrar/tests/test_feature_flags.py @@ -4,7 +4,8 @@ from django.test import TestCase, Client, RequestFactory from registrar.models import ( WaffleFlag, User, - Contact + Contact, + UserGroup, ) from registrar.tests.common import create_superuser, create_staffuser, create_user @@ -23,6 +24,24 @@ class TestFeatureFlags(TestCase): User.objects.all().delete() Contact.objects.all().delete() + def assert_flag_active(self, request_user, flag_name, location="/"): + """ + Checks if the given `request_user` has `flag_name` active + using waffles `flag_is_active` function. + """ + request = self.factory.get(location) + request.user = request_user + self.assertTrue(flag_is_active(request, flag_name)) + + def assert_flag_not_active(self, request_user, flag_name, location="/"): + """ + Checks if the given `request_user` has `flag_name` not active + using waffles `flag_is_active` function. + """ + request = self.factory.get(location) + request.user = request_user + self.assertFalse(flag_is_active(request, flag_name)) + def test_flag_active_for_superuser(self): """ Tests flag_is_active for a flag with `superuser = True` @@ -33,57 +52,110 @@ class TestFeatureFlags(TestCase): staff=False, ) # Test if superusers can access this flag - request = self.factory.get("/") - request.user = self.superuser - self.assertTrue(flag_is_active(request, flag.name)) + self.assert_flag_active(request_user=self.superuser, flag_name=flag.name) # Ensure that regular staff cannot access this flag - request_staff = self.factory.get("/") - request_staff.user = self.staffuser - self.assertFalse(flag_is_active(request_staff, flag.name)) + self.assert_flag_not_active(request_user=self.staffuser, flag_name=flag.name) # Ensure that a normal user also can't access this flag - request_normal = self.factory.get("/") - request_normal.user = self.user - self.assertFalse(flag_is_active(request_normal, flag.name)) + self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) - @skip("not implemented yet") def test_flag_active_for_is_staff(self): """ Tests flag_is_active for a flag with `is_staff = True` """ - # Test if staff can access this flag - # Ensure that superusers cannot - raise - - @skip("not implemented yet") + # We should actually expect superusers + # to not see this feature - otherwise the two distinct booleans aren't useful. + # In practice, we would usually use groups for toggling features. + flag, _ = WaffleFlag.objects.get_or_create( + name="test_superuser_flag", + superusers=False, + staff=True, + ) + + # Ensure that regular staff can access this flag + self.assert_flag_active(request_user=self.staffuser, flag_name=flag.name) + + # Ensure that superusers cannot access this flag + self.assert_flag_not_active(request_user=self.superuser, flag_name=flag.name) + + # Ensure that a normal user also can't access this flag + self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) + def test_flag_active_for_everyone(self): """ Tests flag_is_active for a flag with `everyone = True` """ - # Test if superuser, analyst, and a normal user can access - raise - - @skip("not implemented yet") + flag, _ = WaffleFlag.objects.get_or_create( + name="test_superuser_flag", + everyone=True, + ) + + # Ensure that regular staff can access this flag + self.assert_flag_active(request_user=self.staffuser, flag_name=flag.name) + + # Ensure that superusers can access this flag + self.assert_flag_active(request_user=self.superuser, flag_name=flag.name) + + # Ensure that normal users can access this flag + self.assert_flag_active(request_user=self.user, flag_name=flag.name) + def test_flag_active_for_everyone_is_false(self): """ Tests flag_is_active for a flag with `everyone = False` """ - # Test if no user type can access - raise + flag, _ = WaffleFlag.objects.get_or_create( + name="test_superuser_flag", + everyone=False, + ) + + # Ensure that regular staff cannot access this flag + self.assert_flag_not_active(request_user=self.staffuser, flag_name=flag.name) + + # Ensure that superusers cannot access this flag + self.assert_flag_not_active(request_user=self.superuser, flag_name=flag.name) + + # Ensure that normal users cannot access this flag + self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) - @skip("not implemented yet") def test_admin_group(self): """ Tests flag_is_active for the admin user group """ - # Test if no user type can access - raise + flag, _ = WaffleFlag.objects.get_or_create( + name="test_superuser_flag", + ) + + # Add the full access group to this flag + group, _ = UserGroup.objects.get_or_create(name="full_access_group") + flag.groups.set([group]) + + # Ensure that regular staff cannot access this flag + self.assert_flag_not_active(request_user=self.staffuser, flag_name=flag.name) + + # Ensure that superusers can access this flag + self.assert_flag_active(request_user=self.superuser, flag_name=flag.name) + + # Ensure that normal users cannot access this flag + self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) - @skip("not implemented yet") def test_staff_group(self): """ Tests flag_is_active for the staff user group """ - # Test if no user type can access - raise \ No newline at end of file + flag, _ = WaffleFlag.objects.get_or_create( + name="test_superuser_flag", + ) + + # Add the analyst group to this flag + analyst_group, _ = UserGroup.objects.get_or_create(name="cisa_analysts_group") + flag.groups.set([analyst_group]) + + # Ensure that regular staff can access this flag + self.assert_flag_active(request_user=self.staffuser, flag_name=flag.name) + + # Ensure that superusers cannot access this flag + self.assert_flag_not_active(request_user=self.superuser, flag_name=flag.name) + + # Ensure that normal users cannot access this flag + self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) \ No newline at end of file From 39d51004fd441d51e4e8edc5c4ac40fd267dbc82 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 1 May 2024 08:43:05 -0600 Subject: [PATCH 34/76] Linting --- src/registrar/tests/common.py | 2 ++ src/registrar/tests/test_feature_flags.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index b4c00b725..2715d21b0 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -786,6 +786,7 @@ def create_staffuser(): user.groups.set([group]) return user + def create_user(): """Creates a user with no special permissions""" User = get_user_model() @@ -799,6 +800,7 @@ def create_user(): ) return user + def create_ready_domain(): domain, _ = Domain.objects.get_or_create(name="city.gov", state=Domain.State.READY) return domain diff --git a/src/registrar/tests/test_feature_flags.py b/src/registrar/tests/test_feature_flags.py index c24a07ee8..3cccbfadb 100644 --- a/src/registrar/tests/test_feature_flags.py +++ b/src/registrar/tests/test_feature_flags.py @@ -1,4 +1,3 @@ -from unittest import skip from waffle.decorators import flag_is_active from django.test import TestCase, Client, RequestFactory from registrar.models import ( @@ -9,6 +8,7 @@ from registrar.models import ( ) from registrar.tests.common import create_superuser, create_staffuser, create_user + class TestFeatureFlags(TestCase): def setUp(self): super().setUp() @@ -32,7 +32,7 @@ class TestFeatureFlags(TestCase): request = self.factory.get(location) request.user = request_user self.assertTrue(flag_is_active(request, flag_name)) - + def assert_flag_not_active(self, request_user, flag_name, location="/"): """ Checks if the given `request_user` has `flag_name` not active @@ -59,7 +59,7 @@ class TestFeatureFlags(TestCase): # Ensure that a normal user also can't access this flag self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) - + def test_flag_active_for_is_staff(self): """ Tests flag_is_active for a flag with `is_staff = True` @@ -158,4 +158,4 @@ class TestFeatureFlags(TestCase): self.assert_flag_not_active(request_user=self.superuser, flag_name=flag.name) # Ensure that normal users cannot access this flag - self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) \ No newline at end of file + self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) From 972678d3d2a3154902e9c8cc1ed9b25f1cb65c60 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 1 May 2024 08:48:33 -0600 Subject: [PATCH 35/76] Fux unit tests --- src/registrar/tests/test_feature_flags.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_feature_flags.py b/src/registrar/tests/test_feature_flags.py index 3cccbfadb..7e3ed4880 100644 --- a/src/registrar/tests/test_feature_flags.py +++ b/src/registrar/tests/test_feature_flags.py @@ -15,6 +15,9 @@ class TestFeatureFlags(TestCase): self.client = Client(HTTP_HOST="localhost:8080") self.factory = RequestFactory() self.superuser = create_superuser() + # For testing purposes, lets flag this as false. + self.superuser.is_staff = False + self.superuser.save() self.staffuser = create_staffuser() self.user = create_user() @@ -154,8 +157,9 @@ class TestFeatureFlags(TestCase): # Ensure that regular staff can access this flag self.assert_flag_active(request_user=self.staffuser, flag_name=flag.name) - # Ensure that superusers cannot access this flag - self.assert_flag_not_active(request_user=self.superuser, flag_name=flag.name) + # Ensure that superusers can access this flag. + # This permission encompasses cisa_analysts_group. + self.assert_flag_active(request_user=self.superuser, flag_name=flag.name) # Ensure that normal users cannot access this flag self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) From 8e13a305d5d564a068a58ac0707c381e6165c054 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 1 May 2024 09:18:05 -0600 Subject: [PATCH 36/76] Add waffle sample and switch --- src/registrar/admin.py | 30 ++++--- src/registrar/config/settings.py | 9 ++ ...0_wafflesample_waffleswitch_waffleflag.py} | 89 ++++++++++++++++++- .../0091_create_waffle_flags_v01.py | 3 +- src/registrar/models/__init__.py | 6 ++ src/registrar/models/waffle_flag.py | 8 +- src/registrar/models/waffle_sample.py | 19 ++++ src/registrar/models/waffle_switch.py | 19 ++++ 8 files changed, 167 insertions(+), 16 deletions(-) rename src/registrar/migrations/{0090_waffleflag.py => 0090_wafflesample_waffleswitch_waffleflag.py} (57%) create mode 100644 src/registrar/models/waffle_sample.py create mode 100644 src/registrar/models/waffle_switch.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index bd6666c8b..ee25a3a73 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -15,9 +15,8 @@ from django.contrib.contenttypes.models import ContentType from django.urls import reverse from dateutil.relativedelta import relativedelta # type: ignore from epplibwrapper.errors import ErrorCode, RegistryError -from waffle.admin import FlagAdmin +from waffle.admin import FlagAdmin, SampleAdmin, SwitchAdmin from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website -from waffle.models import Sample, Switch from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.views.utility.mixins import OrderableFieldsMixin from django.contrib.admin.views.main import ORDER_VAR @@ -2168,6 +2167,22 @@ class WaffleFlagAdmin(FlagAdmin): fields = "__all__" +class WaffleSwitchAdmin(SwitchAdmin): + class Meta: + """Contains meta information about this class""" + + model = models.WaffleSwitch + fields = "__all__" + + +class WaffleSampleAdmin(SampleAdmin): + class Meta: + """Contains meta information about this class""" + + model = models.WaffleSample + fields = "__all__" + + admin.site.unregister(LogEntry) # Unregister the default registration admin.site.register(LogEntry, CustomLogEntryAdmin) @@ -2192,12 +2207,7 @@ admin.site.register(models.DomainRequest, DomainRequestAdmin) admin.site.register(models.TransitionDomain, TransitionDomainAdmin) admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) -# Register our custom waffle flag implementation +# Register our custom waffle implementations admin.site.register(models.WaffleFlag, WaffleFlagAdmin) - -# Unregister samples and switches from django-waffle, as we currently don't use these. -# Django admin sorts different "sites" alphabetically, and offers little customization for them. -# If we do need to use these, we should also consider using this library: -# https://pypi.org/project/django-reorder-admin/ -admin.site.unregister(Sample) -admin.site.unregister(Switch) +admin.site.register(models.WaffleSample, WaffleSampleAdmin) +admin.site.register(models.WaffleSwitch, WaffleSwitchAdmin) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index eec184704..b14ea231b 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -328,8 +328,17 @@ SERVER_EMAIL = "root@get.gov" WAFFLE_CREATE_MISSING_FLAGS = False # The model that will be used to keep track of flags. Extends AbstractUserFlag. +# Used to replace the default flag class (for customization purposes). WAFFLE_FLAG_MODEL = "registrar.WaffleFlag" +# The model that will be used to keep track of switches. Extends AbstractBaseSwitch. +# Used to replace the default switch class (for customization purposes). +WAFFLE_SWITCH_MODEL = "registrar.WaffleSwitch" + +# The model that will be used to keep track of samples. Extends AbstractBaseSample. +# Used to replace the default sample class (for customization purposes). +WAFFLE_SAMPLE_MODEL = "registrar.WaffleSample" + # endregion # region: Headers-----------------------------------------------------------### diff --git a/src/registrar/migrations/0090_waffleflag.py b/src/registrar/migrations/0090_wafflesample_waffleswitch_waffleflag.py similarity index 57% rename from src/registrar/migrations/0090_waffleflag.py rename to src/registrar/migrations/0090_wafflesample_waffleswitch_waffleflag.py index 08ea178c6..66c2a22e5 100644 --- a/src/registrar/migrations/0090_waffleflag.py +++ b/src/registrar/migrations/0090_wafflesample_waffleswitch_waffleflag.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-04-30 16:56 +# Generated by Django 4.2.10 on 2024-05-01 15:10 from django.conf import settings from django.db import migrations, models @@ -13,6 +13,93 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name="WaffleSample", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "name", + models.CharField( + help_text="The human/computer readable name.", max_length=100, unique=True, verbose_name="Name" + ), + ), + ( + "percent", + models.DecimalField( + decimal_places=1, + help_text="A number between 0.0 and 100.0 to indicate a percentage of the time this sample will be active.", + max_digits=4, + verbose_name="Percent", + ), + ), + ( + "note", + models.TextField(blank=True, help_text="Note where this Sample is used.", verbose_name="Note"), + ), + ( + "created", + models.DateTimeField( + db_index=True, + default=django.utils.timezone.now, + help_text="Date when this Sample was created.", + verbose_name="Created", + ), + ), + ( + "modified", + models.DateTimeField( + default=django.utils.timezone.now, + help_text="Date when this Sample was last modified.", + verbose_name="Modified", + ), + ), + ], + options={ + "verbose_name": "waffle sample", + "verbose_name_plural": "Waffle samples", + }, + ), + migrations.CreateModel( + name="WaffleSwitch", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "name", + models.CharField( + help_text="The human/computer readable name.", max_length=100, unique=True, verbose_name="Name" + ), + ), + ( + "active", + models.BooleanField(default=False, help_text="Is this switch active?", verbose_name="Active"), + ), + ( + "note", + models.TextField(blank=True, help_text="Note where this Switch is used.", verbose_name="Note"), + ), + ( + "created", + models.DateTimeField( + db_index=True, + default=django.utils.timezone.now, + help_text="Date when this Switch was created.", + verbose_name="Created", + ), + ), + ( + "modified", + models.DateTimeField( + default=django.utils.timezone.now, + help_text="Date when this Switch was last modified.", + verbose_name="Modified", + ), + ), + ], + options={ + "verbose_name": "waffle switch", + "verbose_name_plural": "Waffle switches", + }, + ), migrations.CreateModel( name="WaffleFlag", fields=[ diff --git a/src/registrar/migrations/0091_create_waffle_flags_v01.py b/src/registrar/migrations/0091_create_waffle_flags_v01.py index c643eb12c..6b1bd678d 100644 --- a/src/registrar/migrations/0091_create_waffle_flags_v01.py +++ b/src/registrar/migrations/0091_create_waffle_flags_v01.py @@ -25,6 +25,7 @@ def delete_flags(apps, schema_editor): Deletes all prexisting flags. Does not delete flags not defined in this scope (user generated). """ + # This is a bit of a hack to get around "apps" not knowing what the concept of a constant is default_flags = WaffleFlag.get_default_waffle_flags() WaffleFlag.delete_waffle_flags_for_migrations(apps, default_flags) @@ -32,7 +33,7 @@ def delete_flags(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ("registrar", "0090_waffleflag"), + ("registrar", "0090_wafflesample_waffleswitch_waffleflag"), ] operations = [ diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 14ae25df4..75abb03e9 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -16,6 +16,8 @@ from .website import Website from .transition_domain import TransitionDomain from .verified_by_staff import VerifiedByStaff from .waffle_flag import WaffleFlag +from .waffle_sample import WaffleSample +from .waffle_switch import WaffleSwitch __all__ = [ "Contact", @@ -35,6 +37,8 @@ __all__ = [ "TransitionDomain", "VerifiedByStaff", "WaffleFlag", + "WaffleSwitch", + "WaffleSample", ] auditlog.register(Contact) @@ -54,3 +58,5 @@ auditlog.register(Website) auditlog.register(TransitionDomain) auditlog.register(VerifiedByStaff) auditlog.register(WaffleFlag) +auditlog.register(WaffleSample) +auditlog.register(WaffleSwitch) diff --git a/src/registrar/models/waffle_flag.py b/src/registrar/models/waffle_flag.py index ee6199677..044aa3d37 100644 --- a/src/registrar/models/waffle_flag.py +++ b/src/registrar/models/waffle_flag.py @@ -22,10 +22,10 @@ class WaffleFlag(AbstractUserFlag): def get_default_waffle_flags(): """ Defines which waffle flags should be created at startup. - Add to this list if you want to add another flag that is generated at startup. + + Add to this function if you want to add another flag that is generated at startup. When you do so, you will need to add a new instance of `0091_create_waffle_flags_v{version_number}` in registrar/migrations for that change to update automatically on migrate. - This has to exist here, as from the context of migrations, it cannot access constants """ default_flags = [ # flag_name, flag_note @@ -37,7 +37,7 @@ class WaffleFlag(AbstractUserFlag): @staticmethod def create_waffle_flags_for_migrations(apps, default_waffle_flags): """ - Creates a pre-defined list of flags for our migrations. + Creates a list of flags for our migrations. """ logger.info("Creating default waffle flags...") WaffleFlag = apps.get_model("registrar", "WaffleFlag") @@ -57,7 +57,7 @@ class WaffleFlag(AbstractUserFlag): @staticmethod def delete_waffle_flags_for_migrations(apps, default_waffle_flags): """ - Delete a pre-defined list of flags for our migrations (the reverse_code operation). + Delete a list of flags for our migrations (the reverse_code operation). """ logger.info("Deleting default waffle flags...") WaffleFlag = apps.get_model("registrar", "WaffleFlag") diff --git a/src/registrar/models/waffle_sample.py b/src/registrar/models/waffle_sample.py new file mode 100644 index 000000000..6b2ae59c6 --- /dev/null +++ b/src/registrar/models/waffle_sample.py @@ -0,0 +1,19 @@ +from waffle.models import AbstractBaseSample +import logging + +logger = logging.getLogger(__name__) + + +class WaffleSample(AbstractBaseSample): + """ + Custom implementation of django-waffles 'sample' object. + Read more here: https://waffle.readthedocs.io/en/stable/types/sample.html + + Use this class when dealing with samples. + """ + + class Meta: + """Contains meta information about this class""" + + verbose_name = "waffle sample" + verbose_name_plural = "Waffle samples" diff --git a/src/registrar/models/waffle_switch.py b/src/registrar/models/waffle_switch.py new file mode 100644 index 000000000..640b2b767 --- /dev/null +++ b/src/registrar/models/waffle_switch.py @@ -0,0 +1,19 @@ +from waffle.models import AbstractBaseSwitch +import logging + +logger = logging.getLogger(__name__) + + +class WaffleSwitch(AbstractBaseSwitch): + """ + Custom implementation of django-waffles 'switch' object. + Read more here: https://waffle.readthedocs.io/en/stable/types/switch.html + + Use this class when dealing with switches. + """ + + class Meta: + """Contains meta information about this class""" + + verbose_name = "waffle switch" + verbose_name_plural = "Waffle switches" From 85ed21801038611932ba5a6476f324b2a8e25eeb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 1 May 2024 09:38:09 -0600 Subject: [PATCH 37/76] Update test_feature_flags.py --- src/registrar/tests/test_feature_flags.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_feature_flags.py b/src/registrar/tests/test_feature_flags.py index 7e3ed4880..62e15e3bf 100644 --- a/src/registrar/tests/test_feature_flags.py +++ b/src/registrar/tests/test_feature_flags.py @@ -15,9 +15,11 @@ class TestFeatureFlags(TestCase): self.client = Client(HTTP_HOST="localhost:8080") self.factory = RequestFactory() self.superuser = create_superuser() - # For testing purposes, lets flag this as false. + + # For testing purposes, lets set this to false. self.superuser.is_staff = False self.superuser.save() + self.staffuser = create_staffuser() self.user = create_user() From 4b86f7f830de01d7d3d4083430f653ff97b800b7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 1 May 2024 10:14:19 -0600 Subject: [PATCH 38/76] Fix accidental rename --- ...r_verification_type copy.py => 0089_user_verification_type.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/registrar/migrations/{0089_user_verification_type copy.py => 0089_user_verification_type.py} (100%) diff --git a/src/registrar/migrations/0089_user_verification_type copy.py b/src/registrar/migrations/0089_user_verification_type.py similarity index 100% rename from src/registrar/migrations/0089_user_verification_type copy.py rename to src/registrar/migrations/0089_user_verification_type.py From 984d7fddfe9c93c0a89944cd10b2e1b5c99ffba7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 1 May 2024 10:17:05 -0600 Subject: [PATCH 39/76] Update 0090_wafflesample_waffleswitch_waffleflag.py --- .../migrations/0090_wafflesample_waffleswitch_waffleflag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/migrations/0090_wafflesample_waffleswitch_waffleflag.py b/src/registrar/migrations/0090_wafflesample_waffleswitch_waffleflag.py index 66c2a22e5..3830ca0e4 100644 --- a/src/registrar/migrations/0090_wafflesample_waffleswitch_waffleflag.py +++ b/src/registrar/migrations/0090_wafflesample_waffleswitch_waffleflag.py @@ -9,7 +9,7 @@ class Migration(migrations.Migration): dependencies = [ ("auth", "0012_alter_user_first_name_max_length"), - ("registrar", "0089_user_verification_type copy"), + ("registrar", "0089_user_verification_type"), ] operations = [ From 3e5228a86f62d5fd369218e71b8da4c59a36f875 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 1 May 2024 11:28:20 -0600 Subject: [PATCH 40/76] Add target blank --- .../templates/django/admin/includes/detail_table_fieldset.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html index ea8e7579f..93babb2c4 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -44,7 +44,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% with total_websites=field.contents|split:", " %} {% for website in total_websites %} - {{ website }}{% if not forloop.last %}, {% endif %} + {{ website }}{% if not forloop.last %}, {% endif %} {# Acts as a
#} {% if total_websites|length < 5 %}
From 271641d22cb6a9f3ebc45bb40227670e651bfadf Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 1 May 2024 11:34:55 -0600 Subject: [PATCH 41/76] Update src/registrar/views/index.py --- src/registrar/views/index.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py index a0499f7f9..651507c44 100644 --- a/src/registrar/views/index.py +++ b/src/registrar/views/index.py @@ -22,7 +22,6 @@ def index(request): context["has_deletable_domain_requests"] = has_deletable_domain_requests # This is a django waffle flag which toggles features based off of the "flag" table - # TODO - note that the flag must be checked for superuser AND staff for superuser to see it context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") # If they can delete domain requests, add the delete button to the context From 9a4887ed51e6e7028f5a552eb5b55106eb5485aa Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 1 May 2024 12:29:31 -0600 Subject: [PATCH 42/76] Fix test --- src/registrar/tests/test_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index b42667e01..f7fa0a605 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1797,7 +1797,7 @@ class TestDomainRequestAdmin(MockEppLib): self.assertContains(response, domain_request.requested_domain.name) # Check that the page contains the link we expect. - expected_url = 'city.com' + expected_url = 'city.com' self.assertContains(response, expected_url) @less_console_noise_decorator From c0a967c4ea4a72f42746c2f8bb4a4aeabb0660e4 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 1 May 2024 14:30:08 -0400 Subject: [PATCH 43/76] cleanup --- src/djangooidc/backends.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 81e782c7b..df086059e 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -79,12 +79,14 @@ class OpenIdConnectBackend(ModelBackend): - 'first_name', 'last_name' or 'phone' will be updated if the provided value is not empty. """ + fields_to_check = ["first_name", "last_name", "phone"] + # Iterate over fields to update for key, value in kwargs.items(): # Check if the field is not 'first_name', 'last_name', or 'phone', # or if it's 'first_name' or 'last_name' or 'phone' and the provided value is not empty - if key not in ["first_name", "last_name", "phone"] or ( - key in ["first_name", "last_name", "phone"] and value + if key not in fields_to_check or ( + key in fields_to_check and value ): # Update the corresponding attribute of the user object setattr(user, key, value) From a961328d78407b92daa1e92e06fbce6fccbea920 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 1 May 2024 18:18:21 -0400 Subject: [PATCH 44/76] Add CISA region on domain requests change form --- .../admin/includes/detail_table_fieldset.html | 12 ++++ src/registrar/templatetags/custom_filters.py | 63 +++++++++++++++++++ src/registrar/tests/test_admin.py | 60 ++++++++++++++++++ src/registrar/views/utility/generic_helper.py | 63 +++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 src/registrar/views/utility/generic_helper.py diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html index ea8e7579f..eafec405e 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -1,4 +1,5 @@ {% extends "admin/fieldset.html" %} +{% load custom_filters %} {% load static url_helpers %} {% comment %} @@ -126,5 +127,16 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% endif %} {% endwith %} + {% elif field.field.name == "state_territory" %} +
+ + CISA Region: + {% if original_object.generic_org_type != "federal" %} + {{ original_object.state_territory|get_region }} + {% else %} + N/A + {% endif %} + +
{% endif %} {% endblock after_help_text %} diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index 9fa5c9aa9..af2c15db2 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -67,3 +67,66 @@ def get_organization_long_name(generic_org_type): @register.filter(name="has_permission") def has_permission(user, permission): return user.has_perm(permission) + + +@register.filter +def get_region(state): + regions = { + "CT": 1, + "ME": 1, + "MA": 1, + "NH": 1, + "RI": 1, + "VT": 1, + "NJ": 2, + "NY": 2, + "PR": 2, + "VI": 2, + "DE": 3, + "DC": 3, + "MD": 3, + "PA": 3, + "VA": 3, + "WV": 3, + "AL": 4, + "FL": 4, + "GA": 4, + "KY": 4, + "MS": 4, + "NC": 4, + "SC": 4, + "TN": 4, + "IL": 5, + "IN": 5, + "MI": 5, + "MN": 5, + "OH": 5, + "WI": 5, + "AR": 6, + "LA": 6, + "NM": 6, + "OK": 6, + "TX": 6, + "IA": 7, + "KS": 7, + "MO": 7, + "NE": 7, + "CO": 8, + "MT": 8, + "ND": 8, + "SD": 8, + "UT": 8, + "WY": 8, + "AZ": 9, + "CA": 9, + "HI": 9, + "NV": 9, + "GU": 9, + "AS": 9, + "MP": 9, + "AK": 10, + "ID": 10, + "OR": 10, + "WA": 10, + } + return regions.get(state.upper(), None) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index b42667e01..00e6265a0 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2392,6 +2392,66 @@ class TestDomainRequestAdmin(MockEppLib): self.assertEqual(expected_list, actual_list) + @less_console_noise_decorator + def test_staff_can_see_cisa_region_federal(self): + """Tests if staff can see CISA Region: N/A""" + + # Create a fake domain request + _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + p = "userpass" + self.client.login(username="staffuser", password=p) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, _domain_request.requested_domain.name) + + # Test if the page has the right CISA region + expected_html = '
' "CISA Region: N/A" "
" + # Remove whitespace from expected_html + expected_html = "".join(expected_html.split()) + + # Remove whitespace from response content + response_content = "".join(response.content.decode().split()) + + # Check if response contains expected_html + self.assertIn(expected_html, response_content) + + @less_console_noise_decorator + def test_staff_can_see_cisa_region_non_federal(self): + """Tests if staff can see the correct CISA region""" + + # Create a fake domain request. State will be NY (2). + _domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type="interstate" + ) + + p = "userpass" + self.client.login(username="staffuser", password=p) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, _domain_request.requested_domain.name) + + # Test if the page has the right CISA region + expected_html = '
' 'CISA Region: 2' "
" + # Remove whitespace from expected_html + expected_html = "".join(expected_html.split()) + + # Remove whitespace from response content + response_content = "".join(response.content.decode().split()) + + # Check if response contains expected_html + self.assertIn(expected_html, response_content) + def tearDown(self): super().tearDown() Domain.objects.all().delete() diff --git a/src/registrar/views/utility/generic_helper.py b/src/registrar/views/utility/generic_helper.py new file mode 100644 index 000000000..16ef1435d --- /dev/null +++ b/src/registrar/views/utility/generic_helper.py @@ -0,0 +1,63 @@ +"""This file contains general purpose helpers that don't belong in any specific location""" + + +def get_region(state): + regions = { + "CT": 1, + "ME": 1, + "MA": 1, + "NH": 1, + "RI": 1, + "VT": 1, + "NJ": 2, + "NY": 2, + "PR": 2, + "VI": 2, + "DE": 3, + "DC": 3, + "MD": 3, + "PA": 3, + "VA": 3, + "WV": 3, + "AL": 4, + "FL": 4, + "GA": 4, + "KY": 4, + "MS": 4, + "NC": 4, + "SC": 4, + "TN": 4, + "IL": 5, + "IN": 5, + "MI": 5, + "MN": 5, + "OH": 5, + "WI": 5, + "AR": 6, + "LA": 6, + "NM": 6, + "OK": 6, + "TX": 6, + "IA": 7, + "KS": 7, + "MO": 7, + "NE": 7, + "CO": 8, + "MT": 8, + "ND": 8, + "SD": 8, + "UT": 8, + "WY": 8, + "AZ": 9, + "CA": 9, + "HI": 9, + "NV": 9, + "GU": 9, + "AS": 9, + "MP": 9, + "AK": 10, + "ID": 10, + "OR": 10, + "WA": 10, + } + return regions.get(state.upper(), None) From 501f68ea77a02fe5afc124b24eafa176667769d2 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 1 May 2024 18:22:25 -0400 Subject: [PATCH 45/76] cleanup --- src/registrar/tests/test_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 00e6265a0..49f3f681a 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2411,7 +2411,7 @@ class TestDomainRequestAdmin(MockEppLib): self.assertContains(response, _domain_request.requested_domain.name) # Test if the page has the right CISA region - expected_html = '
' "CISA Region: N/A" "
" + expected_html = '
' "CISA region: N/A" "
" # Remove whitespace from expected_html expected_html = "".join(expected_html.split()) @@ -2442,7 +2442,7 @@ class TestDomainRequestAdmin(MockEppLib): self.assertContains(response, _domain_request.requested_domain.name) # Test if the page has the right CISA region - expected_html = '
' 'CISA Region: 2' "
" + expected_html = '
' 'CISA region: 2' "
" # Remove whitespace from expected_html expected_html = "".join(expected_html.split()) From e454831426ae85c26e192e668e77615634c02c42 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 1 May 2024 18:24:11 -0400 Subject: [PATCH 46/76] cleanup --- src/registrar/views/utility/generic_helper.py | 63 ------------------- 1 file changed, 63 deletions(-) delete mode 100644 src/registrar/views/utility/generic_helper.py diff --git a/src/registrar/views/utility/generic_helper.py b/src/registrar/views/utility/generic_helper.py deleted file mode 100644 index 16ef1435d..000000000 --- a/src/registrar/views/utility/generic_helper.py +++ /dev/null @@ -1,63 +0,0 @@ -"""This file contains general purpose helpers that don't belong in any specific location""" - - -def get_region(state): - regions = { - "CT": 1, - "ME": 1, - "MA": 1, - "NH": 1, - "RI": 1, - "VT": 1, - "NJ": 2, - "NY": 2, - "PR": 2, - "VI": 2, - "DE": 3, - "DC": 3, - "MD": 3, - "PA": 3, - "VA": 3, - "WV": 3, - "AL": 4, - "FL": 4, - "GA": 4, - "KY": 4, - "MS": 4, - "NC": 4, - "SC": 4, - "TN": 4, - "IL": 5, - "IN": 5, - "MI": 5, - "MN": 5, - "OH": 5, - "WI": 5, - "AR": 6, - "LA": 6, - "NM": 6, - "OK": 6, - "TX": 6, - "IA": 7, - "KS": 7, - "MO": 7, - "NE": 7, - "CO": 8, - "MT": 8, - "ND": 8, - "SD": 8, - "UT": 8, - "WY": 8, - "AZ": 9, - "CA": 9, - "HI": 9, - "NV": 9, - "GU": 9, - "AS": 9, - "MP": 9, - "AK": 10, - "ID": 10, - "OR": 10, - "WA": 10, - } - return regions.get(state.upper(), None) From 1821735c8bf89f0a9bbb5b9b40e87b1cba9577e6 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 1 May 2024 18:32:34 -0400 Subject: [PATCH 47/76] cleanup --- .../templates/django/admin/includes/detail_table_fieldset.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html index eafec405e..6954b1fea 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -130,7 +130,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% elif field.field.name == "state_territory" %}
- CISA Region: + CISA region: {% if original_object.generic_org_type != "federal" %} {{ original_object.state_territory|get_region }} {% else %} From f24fd1f510a985847e5322b791281dbe5548d3ee Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 1 May 2024 18:46:21 -0400 Subject: [PATCH 48/76] fix tests --- src/registrar/tests/test_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 49f3f681a..8ed966fc4 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2411,7 +2411,7 @@ class TestDomainRequestAdmin(MockEppLib): self.assertContains(response, _domain_request.requested_domain.name) # Test if the page has the right CISA region - expected_html = '
' "CISA region: N/A" "
" + expected_html = '
CISA region: N/A
' # Remove whitespace from expected_html expected_html = "".join(expected_html.split()) @@ -2442,7 +2442,7 @@ class TestDomainRequestAdmin(MockEppLib): self.assertContains(response, _domain_request.requested_domain.name) # Test if the page has the right CISA region - expected_html = '
' 'CISA region: 2' "
" + expected_html = '
CISA region: 2
' # Remove whitespace from expected_html expected_html = "".join(expected_html.split()) From c204e0e3f4a505c010727ccd3805b10f19b9028d Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Thu, 2 May 2024 09:11:18 -0700 Subject: [PATCH 49/76] Add new developer sandbox 'cb' infrastructure --- .github/workflows/migrate.yaml | 1 + .github/workflows/reset-db.yaml | 1 + ops/manifests/manifest-cb.yaml | 32 ++++++++++++++++++++++++++++++++ src/registrar/config/settings.py | 1 + 4 files changed, 35 insertions(+) create mode 100644 ops/manifests/manifest-cb.yaml diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index 825ab04d7..f5815012c 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -16,6 +16,7 @@ on: - stable - staging - development + - cb - bob - meoward - backup diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index 05eb963c3..06638aa05 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -16,6 +16,7 @@ on: options: - staging - development + - cb - bob - meoward - backup diff --git a/ops/manifests/manifest-cb.yaml b/ops/manifests/manifest-cb.yaml new file mode 100644 index 000000000..b9be98d27 --- /dev/null +++ b/ops/manifests/manifest-cb.yaml @@ -0,0 +1,32 @@ +--- +applications: +- name: getgov-cb + buildpacks: + - python_buildpack + path: ../../src + instances: 1 + memory: 512M + stack: cflinuxfs4 + timeout: 180 + command: ./run.sh + health-check-type: http + health-check-http-endpoint: /health + health-check-invocation-timeout: 40 + env: + # Send stdout and stderr straight to the terminal without buffering + PYTHONUNBUFFERED: yup + # Tell Django where to find its configuration + DJANGO_SETTINGS_MODULE: registrar.config.settings + # Tell Django where it is being hosted + DJANGO_BASE_URL: https://getgov-cb.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO + # default public site location + GETGOV_PUBLIC_SITE_URL: https://get.gov + # Flag to disable/enable features in prod environments + IS_PRODUCTION: False + routes: + - route: getgov-cb.app.cloud.gov + services: + - getgov-credentials + - getgov-cb-database diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 9817476bb..d5a4f203f 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -642,6 +642,7 @@ ALLOWED_HOSTS = [ "getgov-stable.app.cloud.gov", "getgov-staging.app.cloud.gov", "getgov-development.app.cloud.gov", + "getgov-cb.app.cloud.gov", "getgov-bob.app.cloud.gov", "getgov-meoward.app.cloud.gov", "getgov-backup.app.cloud.gov", From e17931517137f5da8e8e9179d7c43df71539edfd Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Thu, 2 May 2024 09:21:41 -0700 Subject: [PATCH 50/76] added cb to deploy --- .github/workflows/deploy-sandbox.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index d9d7cbe14..1c486bdf7 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -24,6 +24,7 @@ jobs: || startsWith(github.head_ref, 'backup/') || startsWith(github.head_ref, 'meoward/') || startsWith(github.head_ref, 'bob/') + || startsWith(github.head_ref, 'cb/') outputs: environment: ${{ steps.var.outputs.environment}} runs-on: "ubuntu-latest" From fcacc0d786400aac83d05cac9bec1d32f500bfda Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 2 May 2024 11:48:53 -0600 Subject: [PATCH 51/76] Remove WaffleSwitch, WaffleSample, and unit tests --- docs/developer/adding-feature-flags.md | 3 - src/registrar/admin.py | 26 +-- src/registrar/config/settings.py | 8 - ...witch_waffleflag.py => 0090_waffleflag.py} | 89 +--------- .../0091_create_waffle_flags_v01.py | 2 +- src/registrar/models/__init__.py | 7 +- src/registrar/models/waffle_flag.py | 4 +- src/registrar/models/waffle_sample.py | 19 -- src/registrar/models/waffle_switch.py | 19 -- src/registrar/tests/test_feature_flags.py | 167 ------------------ 10 files changed, 10 insertions(+), 334 deletions(-) rename src/registrar/migrations/{0090_wafflesample_waffleswitch_waffleflag.py => 0090_waffleflag.py} (57%) delete mode 100644 src/registrar/models/waffle_sample.py delete mode 100644 src/registrar/models/waffle_switch.py delete mode 100644 src/registrar/tests/test_feature_flags.py diff --git a/docs/developer/adding-feature-flags.md b/docs/developer/adding-feature-flags.md index 711b6b7b6..99cede39e 100644 --- a/docs/developer/adding-feature-flags.md +++ b/docs/developer/adding-feature-flags.md @@ -20,9 +20,6 @@ Follow these steps to achieve this: 5. Rename the copied migration to match the increment. For instance, if `0091_create_waffle_flags_v01` exists, you will rename your migration to `0091_create_waffle_flags_v02`. 6. Modify the migration dependency to match the last migration in the stack. -## Modifying an existing feature flag through the CLI -Waffle comes with built in management commands that you can use to update records remotely. [Read here](https://waffle.readthedocs.io/en/stable/usage/cli.html) for information on how to use them. - ## Using feature flags as boolean values Waffle [provides a boolean](https://waffle.readthedocs.io/en/stable/usage/views.html) called `flag_is_active` that you can use as you otherwise would a boolean. This boolean requires a request object and the flag name. diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ee25a3a73..95ec90cb1 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -15,7 +15,8 @@ from django.contrib.contenttypes.models import ContentType from django.urls import reverse from dateutil.relativedelta import relativedelta # type: ignore from epplibwrapper.errors import ErrorCode, RegistryError -from waffle.admin import FlagAdmin, SampleAdmin, SwitchAdmin +from waffle.admin import FlagAdmin +from waffle.models import Sample, Switch from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.views.utility.mixins import OrderableFieldsMixin @@ -2166,23 +2167,6 @@ class WaffleFlagAdmin(FlagAdmin): model = models.WaffleFlag fields = "__all__" - -class WaffleSwitchAdmin(SwitchAdmin): - class Meta: - """Contains meta information about this class""" - - model = models.WaffleSwitch - fields = "__all__" - - -class WaffleSampleAdmin(SampleAdmin): - class Meta: - """Contains meta information about this class""" - - model = models.WaffleSample - fields = "__all__" - - admin.site.unregister(LogEntry) # Unregister the default registration admin.site.register(LogEntry, CustomLogEntryAdmin) @@ -2209,5 +2193,7 @@ admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) # Register our custom waffle implementations admin.site.register(models.WaffleFlag, WaffleFlagAdmin) -admin.site.register(models.WaffleSample, WaffleSampleAdmin) -admin.site.register(models.WaffleSwitch, WaffleSwitchAdmin) + +# Unregister Sample and Switch from the waffle library +admin.site.unregister(Sample) +admin.site.unregister(Switch) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index b14ea231b..2df448d8c 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -331,14 +331,6 @@ WAFFLE_CREATE_MISSING_FLAGS = False # Used to replace the default flag class (for customization purposes). WAFFLE_FLAG_MODEL = "registrar.WaffleFlag" -# The model that will be used to keep track of switches. Extends AbstractBaseSwitch. -# Used to replace the default switch class (for customization purposes). -WAFFLE_SWITCH_MODEL = "registrar.WaffleSwitch" - -# The model that will be used to keep track of samples. Extends AbstractBaseSample. -# Used to replace the default sample class (for customization purposes). -WAFFLE_SAMPLE_MODEL = "registrar.WaffleSample" - # endregion # region: Headers-----------------------------------------------------------### diff --git a/src/registrar/migrations/0090_wafflesample_waffleswitch_waffleflag.py b/src/registrar/migrations/0090_waffleflag.py similarity index 57% rename from src/registrar/migrations/0090_wafflesample_waffleswitch_waffleflag.py rename to src/registrar/migrations/0090_waffleflag.py index 3830ca0e4..3edef9a7e 100644 --- a/src/registrar/migrations/0090_wafflesample_waffleswitch_waffleflag.py +++ b/src/registrar/migrations/0090_waffleflag.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-05-01 15:10 +# Generated by Django 4.2.10 on 2024-05-02 17:47 from django.conf import settings from django.db import migrations, models @@ -13,93 +13,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name="WaffleSample", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "name", - models.CharField( - help_text="The human/computer readable name.", max_length=100, unique=True, verbose_name="Name" - ), - ), - ( - "percent", - models.DecimalField( - decimal_places=1, - help_text="A number between 0.0 and 100.0 to indicate a percentage of the time this sample will be active.", - max_digits=4, - verbose_name="Percent", - ), - ), - ( - "note", - models.TextField(blank=True, help_text="Note where this Sample is used.", verbose_name="Note"), - ), - ( - "created", - models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - help_text="Date when this Sample was created.", - verbose_name="Created", - ), - ), - ( - "modified", - models.DateTimeField( - default=django.utils.timezone.now, - help_text="Date when this Sample was last modified.", - verbose_name="Modified", - ), - ), - ], - options={ - "verbose_name": "waffle sample", - "verbose_name_plural": "Waffle samples", - }, - ), - migrations.CreateModel( - name="WaffleSwitch", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "name", - models.CharField( - help_text="The human/computer readable name.", max_length=100, unique=True, verbose_name="Name" - ), - ), - ( - "active", - models.BooleanField(default=False, help_text="Is this switch active?", verbose_name="Active"), - ), - ( - "note", - models.TextField(blank=True, help_text="Note where this Switch is used.", verbose_name="Note"), - ), - ( - "created", - models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - help_text="Date when this Switch was created.", - verbose_name="Created", - ), - ), - ( - "modified", - models.DateTimeField( - default=django.utils.timezone.now, - help_text="Date when this Switch was last modified.", - verbose_name="Modified", - ), - ), - ], - options={ - "verbose_name": "waffle switch", - "verbose_name_plural": "Waffle switches", - }, - ), migrations.CreateModel( name="WaffleFlag", fields=[ diff --git a/src/registrar/migrations/0091_create_waffle_flags_v01.py b/src/registrar/migrations/0091_create_waffle_flags_v01.py index 6b1bd678d..ca7fcb751 100644 --- a/src/registrar/migrations/0091_create_waffle_flags_v01.py +++ b/src/registrar/migrations/0091_create_waffle_flags_v01.py @@ -33,7 +33,7 @@ def delete_flags(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ("registrar", "0090_wafflesample_waffleswitch_waffleflag"), + ("registrar", "0090_waffleflag"), ] operations = [ diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 75abb03e9..f084a5d8b 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -16,8 +16,7 @@ from .website import Website from .transition_domain import TransitionDomain from .verified_by_staff import VerifiedByStaff from .waffle_flag import WaffleFlag -from .waffle_sample import WaffleSample -from .waffle_switch import WaffleSwitch + __all__ = [ "Contact", @@ -37,8 +36,6 @@ __all__ = [ "TransitionDomain", "VerifiedByStaff", "WaffleFlag", - "WaffleSwitch", - "WaffleSample", ] auditlog.register(Contact) @@ -58,5 +55,3 @@ auditlog.register(Website) auditlog.register(TransitionDomain) auditlog.register(VerifiedByStaff) auditlog.register(WaffleFlag) -auditlog.register(WaffleSample) -auditlog.register(WaffleSwitch) diff --git a/src/registrar/models/waffle_flag.py b/src/registrar/models/waffle_flag.py index 044aa3d37..6d2b3a9a8 100644 --- a/src/registrar/models/waffle_flag.py +++ b/src/registrar/models/waffle_flag.py @@ -41,9 +41,7 @@ class WaffleFlag(AbstractUserFlag): """ logger.info("Creating default waffle flags...") WaffleFlag = apps.get_model("registrar", "WaffleFlag") - # Flags can be changed through the command line or through django admin. - # To keep the scope of this function minimal and simple, if we require additional - # config on these flag, it should be done in a seperate function or as a command. + # Flags can be changed through django admin if necessary. for flag_name, flag_note in default_waffle_flags: try: WaffleFlag.objects.update_or_create( diff --git a/src/registrar/models/waffle_sample.py b/src/registrar/models/waffle_sample.py deleted file mode 100644 index 6b2ae59c6..000000000 --- a/src/registrar/models/waffle_sample.py +++ /dev/null @@ -1,19 +0,0 @@ -from waffle.models import AbstractBaseSample -import logging - -logger = logging.getLogger(__name__) - - -class WaffleSample(AbstractBaseSample): - """ - Custom implementation of django-waffles 'sample' object. - Read more here: https://waffle.readthedocs.io/en/stable/types/sample.html - - Use this class when dealing with samples. - """ - - class Meta: - """Contains meta information about this class""" - - verbose_name = "waffle sample" - verbose_name_plural = "Waffle samples" diff --git a/src/registrar/models/waffle_switch.py b/src/registrar/models/waffle_switch.py deleted file mode 100644 index 640b2b767..000000000 --- a/src/registrar/models/waffle_switch.py +++ /dev/null @@ -1,19 +0,0 @@ -from waffle.models import AbstractBaseSwitch -import logging - -logger = logging.getLogger(__name__) - - -class WaffleSwitch(AbstractBaseSwitch): - """ - Custom implementation of django-waffles 'switch' object. - Read more here: https://waffle.readthedocs.io/en/stable/types/switch.html - - Use this class when dealing with switches. - """ - - class Meta: - """Contains meta information about this class""" - - verbose_name = "waffle switch" - verbose_name_plural = "Waffle switches" diff --git a/src/registrar/tests/test_feature_flags.py b/src/registrar/tests/test_feature_flags.py deleted file mode 100644 index 62e15e3bf..000000000 --- a/src/registrar/tests/test_feature_flags.py +++ /dev/null @@ -1,167 +0,0 @@ -from waffle.decorators import flag_is_active -from django.test import TestCase, Client, RequestFactory -from registrar.models import ( - WaffleFlag, - User, - Contact, - UserGroup, -) -from registrar.tests.common import create_superuser, create_staffuser, create_user - - -class TestFeatureFlags(TestCase): - def setUp(self): - super().setUp() - self.client = Client(HTTP_HOST="localhost:8080") - self.factory = RequestFactory() - self.superuser = create_superuser() - - # For testing purposes, lets set this to false. - self.superuser.is_staff = False - self.superuser.save() - - self.staffuser = create_staffuser() - self.user = create_user() - - def tearDown(self): - super().tearDown() - WaffleFlag.objects.all().delete() - User.objects.all().delete() - Contact.objects.all().delete() - - def assert_flag_active(self, request_user, flag_name, location="/"): - """ - Checks if the given `request_user` has `flag_name` active - using waffles `flag_is_active` function. - """ - request = self.factory.get(location) - request.user = request_user - self.assertTrue(flag_is_active(request, flag_name)) - - def assert_flag_not_active(self, request_user, flag_name, location="/"): - """ - Checks if the given `request_user` has `flag_name` not active - using waffles `flag_is_active` function. - """ - request = self.factory.get(location) - request.user = request_user - self.assertFalse(flag_is_active(request, flag_name)) - - def test_flag_active_for_superuser(self): - """ - Tests flag_is_active for a flag with `superuser = True` - """ - flag, _ = WaffleFlag.objects.get_or_create( - name="test_superuser_flag", - superusers=True, - staff=False, - ) - # Test if superusers can access this flag - self.assert_flag_active(request_user=self.superuser, flag_name=flag.name) - - # Ensure that regular staff cannot access this flag - self.assert_flag_not_active(request_user=self.staffuser, flag_name=flag.name) - - # Ensure that a normal user also can't access this flag - self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) - - def test_flag_active_for_is_staff(self): - """ - Tests flag_is_active for a flag with `is_staff = True` - """ - # We should actually expect superusers - # to not see this feature - otherwise the two distinct booleans aren't useful. - # In practice, we would usually use groups for toggling features. - flag, _ = WaffleFlag.objects.get_or_create( - name="test_superuser_flag", - superusers=False, - staff=True, - ) - - # Ensure that regular staff can access this flag - self.assert_flag_active(request_user=self.staffuser, flag_name=flag.name) - - # Ensure that superusers cannot access this flag - self.assert_flag_not_active(request_user=self.superuser, flag_name=flag.name) - - # Ensure that a normal user also can't access this flag - self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) - - def test_flag_active_for_everyone(self): - """ - Tests flag_is_active for a flag with `everyone = True` - """ - flag, _ = WaffleFlag.objects.get_or_create( - name="test_superuser_flag", - everyone=True, - ) - - # Ensure that regular staff can access this flag - self.assert_flag_active(request_user=self.staffuser, flag_name=flag.name) - - # Ensure that superusers can access this flag - self.assert_flag_active(request_user=self.superuser, flag_name=flag.name) - - # Ensure that normal users can access this flag - self.assert_flag_active(request_user=self.user, flag_name=flag.name) - - def test_flag_active_for_everyone_is_false(self): - """ - Tests flag_is_active for a flag with `everyone = False` - """ - flag, _ = WaffleFlag.objects.get_or_create( - name="test_superuser_flag", - everyone=False, - ) - - # Ensure that regular staff cannot access this flag - self.assert_flag_not_active(request_user=self.staffuser, flag_name=flag.name) - - # Ensure that superusers cannot access this flag - self.assert_flag_not_active(request_user=self.superuser, flag_name=flag.name) - - # Ensure that normal users cannot access this flag - self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) - - def test_admin_group(self): - """ - Tests flag_is_active for the admin user group - """ - flag, _ = WaffleFlag.objects.get_or_create( - name="test_superuser_flag", - ) - - # Add the full access group to this flag - group, _ = UserGroup.objects.get_or_create(name="full_access_group") - flag.groups.set([group]) - - # Ensure that regular staff cannot access this flag - self.assert_flag_not_active(request_user=self.staffuser, flag_name=flag.name) - - # Ensure that superusers can access this flag - self.assert_flag_active(request_user=self.superuser, flag_name=flag.name) - - # Ensure that normal users cannot access this flag - self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) - - def test_staff_group(self): - """ - Tests flag_is_active for the staff user group - """ - flag, _ = WaffleFlag.objects.get_or_create( - name="test_superuser_flag", - ) - - # Add the analyst group to this flag - analyst_group, _ = UserGroup.objects.get_or_create(name="cisa_analysts_group") - flag.groups.set([analyst_group]) - - # Ensure that regular staff can access this flag - self.assert_flag_active(request_user=self.staffuser, flag_name=flag.name) - - # Ensure that superusers can access this flag. - # This permission encompasses cisa_analysts_group. - self.assert_flag_active(request_user=self.superuser, flag_name=flag.name) - - # Ensure that normal users cannot access this flag - self.assert_flag_not_active(request_user=self.user, flag_name=flag.name) From 7c89edce09a531db3957f9a9cd20d6a7aefad5c6 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 2 May 2024 11:52:43 -0600 Subject: [PATCH 52/76] Linting --- src/registrar/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 95ec90cb1..3d97d082c 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2167,6 +2167,7 @@ class WaffleFlagAdmin(FlagAdmin): model = models.WaffleFlag fields = "__all__" + admin.site.unregister(LogEntry) # Unregister the default registration admin.site.register(LogEntry, CustomLogEntryAdmin) From b198120ef8d9979e1acb4debf4aa31d1114dedfe Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 2 May 2024 14:19:15 -0400 Subject: [PATCH 53/76] handle case where state is empty --- src/registrar/templatetags/custom_filters.py | 122 ++++++++++--------- 1 file changed, 63 insertions(+), 59 deletions(-) diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index af2c15db2..169064cb8 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -71,62 +71,66 @@ def has_permission(user, permission): @register.filter def get_region(state): - regions = { - "CT": 1, - "ME": 1, - "MA": 1, - "NH": 1, - "RI": 1, - "VT": 1, - "NJ": 2, - "NY": 2, - "PR": 2, - "VI": 2, - "DE": 3, - "DC": 3, - "MD": 3, - "PA": 3, - "VA": 3, - "WV": 3, - "AL": 4, - "FL": 4, - "GA": 4, - "KY": 4, - "MS": 4, - "NC": 4, - "SC": 4, - "TN": 4, - "IL": 5, - "IN": 5, - "MI": 5, - "MN": 5, - "OH": 5, - "WI": 5, - "AR": 6, - "LA": 6, - "NM": 6, - "OK": 6, - "TX": 6, - "IA": 7, - "KS": 7, - "MO": 7, - "NE": 7, - "CO": 8, - "MT": 8, - "ND": 8, - "SD": 8, - "UT": 8, - "WY": 8, - "AZ": 9, - "CA": 9, - "HI": 9, - "NV": 9, - "GU": 9, - "AS": 9, - "MP": 9, - "AK": 10, - "ID": 10, - "OR": 10, - "WA": 10, - } - return regions.get(state.upper(), None) + if state and isinstance(state, str): + regions = { + "CT": 1, + "ME": 1, + "MA": 1, + "NH": 1, + "RI": 1, + "VT": 1, + "NJ": 2, + "NY": 2, + "PR": 2, + "VI": 2, + "DE": 3, + "DC": 3, + "MD": 3, + "PA": 3, + "VA": 3, + "WV": 3, + "AL": 4, + "FL": 4, + "GA": 4, + "KY": 4, + "MS": 4, + "NC": 4, + "SC": 4, + "TN": 4, + "IL": 5, + "IN": 5, + "MI": 5, + "MN": 5, + "OH": 5, + "WI": 5, + "AR": 6, + "LA": 6, + "NM": 6, + "OK": 6, + "TX": 6, + "IA": 7, + "KS": 7, + "MO": 7, + "NE": 7, + "CO": 8, + "MT": 8, + "ND": 8, + "SD": 8, + "UT": 8, + "WY": 8, + "AZ": 9, + "CA": 9, + "HI": 9, + "NV": 9, + "GU": 9, + "AS": 9, + "MP": 9, + "AK": 10, + "ID": 10, + "OR": 10, + "WA": 10, + } + return regions.get(state.upper(), None) + else: + return None + From a502be9c40ab63f9181e0afb815b3ca8945039a8 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 2 May 2024 14:22:52 -0400 Subject: [PATCH 54/76] cleanup --- src/registrar/templatetags/custom_filters.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index 169064cb8..ff350197c 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -133,4 +133,3 @@ def get_region(state): return regions.get(state.upper(), None) else: return None - From c41b21aa8ebdc0802de7882d433f7a4b172c161a Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 2 May 2024 14:37:32 -0400 Subject: [PATCH 55/76] push missing JS --- src/registrar/assets/js/dja-collapse.js | 45 +++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/registrar/assets/js/dja-collapse.js diff --git a/src/registrar/assets/js/dja-collapse.js b/src/registrar/assets/js/dja-collapse.js new file mode 100644 index 000000000..e1a16cc1a --- /dev/null +++ b/src/registrar/assets/js/dja-collapse.js @@ -0,0 +1,45 @@ +/* + * We will run our own version of + * https://github.com/django/django/blob/195d885ca01b14e3ce9a1881c3b8f7074f953736/django/contrib/admin/static/admin/js/collapse.js + * Works with our fieldset override +*/ + +/*global gettext*/ +'use strict'; +{ + window.addEventListener('load', function() { + // Add anchor tag for Show/Hide link + const fieldsets = document.querySelectorAll('fieldset.collapse--dotgov'); + for (const [i, elem] of fieldsets.entries()) { + // Don't hide if fields in this fieldset have errors + if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) { + elem.classList.add('collapsed'); + const button = elem.querySelector('button'); + button.id = 'fieldsetcollapser' + i; + button.className = 'collapse-toggle--dotgov usa-button usa-button--unstyled'; + } + } + // Add toggle to hide/show anchor tag + const toggleFuncDotgov = function(e) { + e.preventDefault(); + e.stopPropagation(); + const fieldset = this.closest('fieldset'); + const spanElement = this.querySelector('span'); + const useElement = this.querySelector('use'); + if (fieldset.classList.contains('collapsed')) { + // Show + spanElement.textContent = 'Hide details'; + useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less'); + fieldset.classList.remove('collapsed'); + } else { + // Hide + spanElement.textContent = 'Show details'; + useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more'); + fieldset.classList.add('collapsed'); + } + }; + document.querySelectorAll('.collapse-toggle--dotgov').forEach(function(el) { + el.addEventListener('click', toggleFuncDotgov); + }); + }); +} \ No newline at end of file From 550e6df8b129833d1053e45f8ff103baf99c0dad Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 2 May 2024 14:49:40 -0400 Subject: [PATCH 56/76] lint --- src/djangooidc/backends.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index df086059e..41e442f2d 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -85,9 +85,7 @@ class OpenIdConnectBackend(ModelBackend): for key, value in kwargs.items(): # Check if the field is not 'first_name', 'last_name', or 'phone', # or if it's 'first_name' or 'last_name' or 'phone' and the provided value is not empty - if key not in fields_to_check or ( - key in fields_to_check and value - ): + if key not in fields_to_check or (key in fields_to_check and value): # Update the corresponding attribute of the user object setattr(user, key, value) From e521309c43a6540cd763185ebf2ecfec09d8bfc3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 2 May 2024 14:53:15 -0600 Subject: [PATCH 57/76] Remove unused migration --- docs/developer/adding-feature-flags.md | 16 +++---- src/registrar/config/settings.py | 2 +- .../0091_create_waffle_flags_v01.py | 45 ----------------- src/registrar/models/waffle_flag.py | 48 ------------------- 4 files changed, 7 insertions(+), 104 deletions(-) delete mode 100644 src/registrar/migrations/0091_create_waffle_flags_v01.py diff --git a/docs/developer/adding-feature-flags.md b/docs/developer/adding-feature-flags.md index 99cede39e..dd97c7497 100644 --- a/docs/developer/adding-feature-flags.md +++ b/docs/developer/adding-feature-flags.md @@ -9,16 +9,12 @@ We use [django-waffle](https://waffle.readthedocs.io/en/stable/) for our feature 3. Click `Add waffle flag`. 4. Add the model as you would normally. Refer to waffle's documentation [regarding attributes](https://waffle.readthedocs.io/en/stable/types/flag.html#flag-attributes) for more information on them. -## Adding feature flags when migrations are ran -Given that we store waffle flags as a predefined list, this means that we need to create a new migration file when we want to add a set of feature flags programatically this way. Note that if `WAFFLE_CREATE_MISSING_FLAGS` is set to True, you may not need this step. - -Follow these steps to achieve this: -1. Navigate to `registrar/models/waffle_flag.py`. -2. Modify the `get_default_waffle_flags` and add the desired name of your feature flag to the `default_flags` array. -3. Navigate to `registrar/migrationdata`. -4. Copy the migration named `0091_create_waffle_flags_v01`. -5. Rename the copied migration to match the increment. For instance, if `0091_create_waffle_flags_v01` exists, you will rename your migration to `0091_create_waffle_flags_v02`. -6. Modify the migration dependency to match the last migration in the stack. +### Enabling the profile_feature flag +1. On the app, navigate to `\admin`. +2. Under models, click `Waffle flags`. +3. Click the `profile_feature` record. This should exist by default, if not - create one with that name. +4. (Important) Set the field `Everyone` to `Unknown`. This field overrides all other settings when set to anything else. +5. Configure the settings as you see fit. ## Using feature flags as boolean values Waffle [provides a boolean](https://waffle.readthedocs.io/en/stable/usage/views.html) called `flag_is_active` that you can use as you otherwise would a boolean. This boolean requires a request object and the flag name. diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 2df448d8c..ff56f24ea 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -325,7 +325,7 @@ SERVER_EMAIL = "root@get.gov" # region: Waffle feature flags-----------------------------------------------------------### # If Waffle encounters a reference to a flag that is not in the database, should Waffle create the flag? -WAFFLE_CREATE_MISSING_FLAGS = False +WAFFLE_CREATE_MISSING_FLAGS = True # The model that will be used to keep track of flags. Extends AbstractUserFlag. # Used to replace the default flag class (for customization purposes). diff --git a/src/registrar/migrations/0091_create_waffle_flags_v01.py b/src/registrar/migrations/0091_create_waffle_flags_v01.py deleted file mode 100644 index ca7fcb751..000000000 --- a/src/registrar/migrations/0091_create_waffle_flags_v01.py +++ /dev/null @@ -1,45 +0,0 @@ -# This migration creates default WaffleFlag objects for our DB. -# Whenever you add to the `create_waffle_flags` function, increment/copy this -# migration by one - -from django.db import migrations -from registrar.models import WaffleFlag -from typing import Any - - -# For linting: RunPython expects a function reference, -# so let's give it one -def create_flags(apps, schema_editor): - """ - Populates pre-existing flags we wish to associate. - Only generates a flag name and a note, but no other data is loaded at this point. - """ - - # This is a bit of a hack to get around "apps" not knowing what the concept of a constant is - default_flags = WaffleFlag.get_default_waffle_flags() - WaffleFlag.create_waffle_flags_for_migrations(apps, default_flags) - - -def delete_flags(apps, schema_editor): - """ - Deletes all prexisting flags. - Does not delete flags not defined in this scope (user generated). - """ - - # This is a bit of a hack to get around "apps" not knowing what the concept of a constant is - default_flags = WaffleFlag.get_default_waffle_flags() - WaffleFlag.delete_waffle_flags_for_migrations(apps, default_flags) - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0090_waffleflag"), - ] - - operations = [ - migrations.RunPython( - code=create_flags, - reverse_code=delete_flags, - atomic=True, - ), - ] diff --git a/src/registrar/models/waffle_flag.py b/src/registrar/models/waffle_flag.py index 6d2b3a9a8..d185c2a82 100644 --- a/src/registrar/models/waffle_flag.py +++ b/src/registrar/models/waffle_flag.py @@ -17,51 +17,3 @@ class WaffleFlag(AbstractUserFlag): verbose_name = "waffle flag" verbose_name_plural = "Waffle flags" - - @staticmethod - def get_default_waffle_flags(): - """ - Defines which waffle flags should be created at startup. - - Add to this function if you want to add another flag that is generated at startup. - When you do so, you will need to add a new instance of `0091_create_waffle_flags_v{version_number}` - in registrar/migrations for that change to update automatically on migrate. - """ - default_flags = [ - # flag_name, flag_note - ("profile_feature", "Used for profiles"), - ("dns_hosting_feature", "Used for dns hosting"), - ] - return default_flags - - @staticmethod - def create_waffle_flags_for_migrations(apps, default_waffle_flags): - """ - Creates a list of flags for our migrations. - """ - logger.info("Creating default waffle flags...") - WaffleFlag = apps.get_model("registrar", "WaffleFlag") - # Flags can be changed through django admin if necessary. - for flag_name, flag_note in default_waffle_flags: - try: - WaffleFlag.objects.update_or_create( - name=flag_name, - # Booleans like superusers or is_staff can be set here, if needed. - defaults={"note": flag_note}, - ) - except Exception as e: - logger.error(f"An error occurred when attempting to add or update flag {flag_name}: {e}") - - @staticmethod - def delete_waffle_flags_for_migrations(apps, default_waffle_flags): - """ - Delete a list of flags for our migrations (the reverse_code operation). - """ - logger.info("Deleting default waffle flags...") - WaffleFlag = apps.get_model("registrar", "WaffleFlag") - existing_flags = WaffleFlag.objects.filter(name__in=default_waffle_flags) - for flag in existing_flags: - try: - WaffleFlag.objects.get(name=flag.name).delete() - except Exception as e: - logger.error(f"An error occurred when attempting to delete flag {flag.name}: {e}") From 093f59604128855ecde1e1e503ead41092841c18 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 2 May 2024 15:03:08 -0600 Subject: [PATCH 58/76] Revert changes --- .../templates/includes/non-production-alert.html | 2 +- src/registrar/tests/test_admin.py | 14 +++++++------- src/registrar/tests/test_views_domain.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/registrar/templates/includes/non-production-alert.html b/src/registrar/templates/includes/non-production-alert.html index ca7337777..661fb1397 100644 --- a/src/registrar/templates/includes/non-production-alert.html +++ b/src/registrar/templates/includes/non-production-alert.html @@ -7,4 +7,4 @@ {% endif %}
-
\ No newline at end of file +
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 9250b1f2b..2229aab85 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -72,7 +72,7 @@ class TestDomainAdmin(MockEppLib, WebTest): self.admin = DomainAdmin(model=Domain, admin_site=self.site) self.client = Client(HTTP_HOST="localhost:8080") self.superuser = create_superuser() - self.staffuser = create_staffuser() + self.staffuser = create_user() self.factory = RequestFactory() self.app.set_user(self.superuser.username) self.client.force_login(self.superuser) @@ -841,7 +841,7 @@ class TestDomainRequestAdmin(MockEppLib): self.factory = RequestFactory() self.admin = DomainRequestAdmin(model=DomainRequest, admin_site=self.site) self.superuser = create_superuser() - self.staffuser = create_staffuser() + self.staffuser = create_user() self.client = Client(HTTP_HOST="localhost:8080") self.test_helper = GenericTestHelper( factory=self.factory, @@ -2502,7 +2502,7 @@ class TestDomainInformationAdmin(TestCase): self.admin = DomainInformationAdmin(model=DomainInformation, admin_site=self.site) self.client = Client(HTTP_HOST="localhost:8080") self.superuser = create_superuser() - self.staffuser = create_staffuser() + self.staffuser = create_user() self.mock_data_generator = AuditedAdminMockData() self.test_helper = GenericTestHelper( @@ -3012,7 +3012,7 @@ class TestMyUserAdmin(TestCase): """ Tests for the correct helper text on this page """ - user = create_staffuser() + user = create_user() p = "adminpass" self.client.login(username="superuser", password=p) @@ -3036,7 +3036,7 @@ class TestMyUserAdmin(TestCase): def test_list_display_without_username(self): with less_console_noise(): request = self.client.request().wsgi_request - request.user = create_staffuser() + request.user = create_user() list_display = self.admin.get_list_display(request) expected_list_display = [ @@ -3062,7 +3062,7 @@ class TestMyUserAdmin(TestCase): def test_get_fieldsets_cisa_analyst(self): with less_console_noise(): request = self.client.request().wsgi_request - request.user = create_staffuser() + request.user = create_user() fieldsets = self.admin.get_fieldsets(request) expected_fieldsets = ( ( @@ -3448,7 +3448,7 @@ class ContactAdminTest(TestCase): self.client = Client(HTTP_HOST="localhost:8080") self.admin = ContactAdmin(model=get_user_model(), admin_site=None) self.superuser = create_superuser() - self.staffuser = create_staffuser() + self.staffuser = create_user() def test_readonly_when_restricted_staffuser(self): with less_console_noise(): diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index ea93bee62..d207f93bb 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -322,7 +322,7 @@ class TestDomainDetail(TestDomainOverview): when no domain information or domain request exist""" with less_console_noise(): # have to use staff user for this test - staff_user = create_staffuser() + staff_user = create_user() # staff_user.save() self.client.force_login(staff_user) From 44dec2b73a37388d5c69ceab603a530e45bc8656 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 2 May 2024 15:06:51 -0600 Subject: [PATCH 59/76] common.py update --- src/registrar/tests/common.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 2715d21b0..c2f11b85d 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -753,14 +753,12 @@ def mock_user(): def create_superuser(): - """Creates a user with admin-level permissions""" User = get_user_model() p = "adminpass" user = User.objects.create_user( username="superuser", email="admin@example.com", is_staff=True, - is_superuser=True, password=p, ) # Retrieve the group or create it if it doesn't exist @@ -770,8 +768,7 @@ def create_superuser(): return user -def create_staffuser(): - """Creates a user with staff level permissions""" +def create_user(): User = get_user_model() p = "userpass" user = User.objects.create_user( @@ -787,25 +784,12 @@ def create_staffuser(): return user -def create_user(): - """Creates a user with no special permissions""" - User = get_user_model() - p = "userpass" - user = User.objects.create_user( - username="regularuser", - email="regularuser@example.com", - is_staff=False, - is_superuser=False, - password=p, - ) - return user - - def create_ready_domain(): domain, _ = Domain.objects.get_or_create(name="city.gov", state=Domain.State.READY) return domain +# TODO in 1793: Remove the federal agency/updated federal agency fields def completed_domain_request( has_other_contacts=True, has_current_website=True, @@ -820,6 +804,8 @@ def completed_domain_request( generic_org_type="federal", is_election_board=False, organization_type=None, + federal_agency=None, + updated_federal_agency=None, ): """A completed domain request.""" if not user: @@ -856,6 +842,7 @@ def completed_domain_request( last_name="Bob", is_staff=True, ) + domain_request_kwargs = dict( generic_org_type=generic_org_type, is_election_board=is_election_board, @@ -873,6 +860,8 @@ def completed_domain_request( creator=user, status=status, investigator=investigator, + federal_agency=federal_agency, + updated_federal_agency=updated_federal_agency, ) if has_about_your_organization: domain_request_kwargs["about_your_organization"] = "e-Government" @@ -881,7 +870,6 @@ def completed_domain_request( if organization_type: domain_request_kwargs["organization_type"] = organization_type - domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs) if has_other_contacts: From 555bee2c8264f30d2ca3f7766f7ee2918ba4dd5f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 2 May 2024 15:09:55 -0600 Subject: [PATCH 60/76] Update common.py --- src/registrar/tests/common.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index c2f11b85d..c809c46b4 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -789,7 +789,6 @@ def create_ready_domain(): return domain -# TODO in 1793: Remove the federal agency/updated federal agency fields def completed_domain_request( has_other_contacts=True, has_current_website=True, @@ -804,8 +803,6 @@ def completed_domain_request( generic_org_type="federal", is_election_board=False, organization_type=None, - federal_agency=None, - updated_federal_agency=None, ): """A completed domain request.""" if not user: @@ -842,7 +839,6 @@ def completed_domain_request( last_name="Bob", is_staff=True, ) - domain_request_kwargs = dict( generic_org_type=generic_org_type, is_election_board=is_election_board, @@ -860,8 +856,6 @@ def completed_domain_request( creator=user, status=status, investigator=investigator, - federal_agency=federal_agency, - updated_federal_agency=updated_federal_agency, ) if has_about_your_organization: domain_request_kwargs["about_your_organization"] = "e-Government" From 135aa5fb67f66166c2cd6e3bf7a5945f10065b88 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 2 May 2024 15:11:38 -0600 Subject: [PATCH 61/76] Remove changes --- src/registrar/tests/test_admin.py | 2 +- src/registrar/tests/test_views_domain.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 2229aab85..b42667e01 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -44,7 +44,7 @@ from .common import ( less_console_noise, mock_user, create_superuser, - create_staffuser, + create_user, create_ready_domain, multiple_unalphabetical_domain_objects, MockEppLib, diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index d207f93bb..6448e91e1 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -5,7 +5,7 @@ from django.conf import settings from django.urls import reverse from django.contrib.auth import get_user_model -from .common import MockEppLib, MockSESClient, create_staffuser # type: ignore +from .common import MockEppLib, MockSESClient, create_user # type: ignore from django_webtest import WebTest # type: ignore import boto3_mocking # type: ignore From 7f92aaf55bf085c1bbd438020804a00372d244df Mon Sep 17 00:00:00 2001 From: Rachid Mrad <107004823+rachidatecs@users.noreply.github.com> Date: Thu, 2 May 2024 18:11:01 -0400 Subject: [PATCH 62/76] Update src/registrar/templates/django/admin/includes/detail_table_fieldset.html Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- .../templates/django/admin/includes/detail_table_fieldset.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html index 6954b1fea..adbe3f7d1 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -131,7 +131,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
CISA region: - {% if original_object.generic_org_type != "federal" %} + {% if original_object.generic_org_type and original_object.generic_org_type != original_object.OrganizationChoices.FEDERAL %} {{ original_object.state_territory|get_region }} {% else %} N/A From 1b870d194b0946b10ea9f8b5ec50dc49d1ba0959 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 3 May 2024 08:25:38 -0600 Subject: [PATCH 63/76] css changes and add descriptions to files --- src/registrar/assets/sass/_theme/_admin.scss | 4 +- .../templates/admin/model_descriptions.html | 197 ++---------------- .../descriptions/contact_description.html | 7 + .../descriptions/domain_description.html | 25 +++ .../domain_information_description.html | 19 ++ .../domain_invitation_description.html | 16 ++ .../domain_request_description.html | 11 + .../draft_domain_description.html | 10 + .../descriptions/host_description.html | 11 + .../descriptions/logentry_description.html | 7 + .../public_contact_description.html | 18 ++ .../transition_domain_description.html | 4 + .../descriptions/user_description.html | 16 ++ .../user_domain_role_description.html | 10 + .../descriptions/user_group_description.html | 10 + .../verified_by_staff_description.html | 10 + .../admin/includes/descriptions/website.html | 8 + 17 files changed, 199 insertions(+), 184 deletions(-) create mode 100644 src/registrar/templates/django/admin/includes/descriptions/contact_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/domain_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/domain_information_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/domain_request_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/draft_domain_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/host_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/logentry_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/public_contact_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/transition_domain_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/user_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/user_domain_role_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/user_group_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/verified_by_staff_description.html create mode 100644 src/registrar/templates/django/admin/includes/descriptions/website.html diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 9100fb945..0e6768268 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -679,7 +679,7 @@ div.dja__model-description{ @media (prefers-color-scheme: dark) { p, li { - color: var(--primary); + color: var(--body-bg); } } @@ -689,7 +689,7 @@ div.dja__model-description{ &.dja__model-description--no-overflow { display: block; - overflow: unset; + overflow: auto; } } diff --git a/src/registrar/templates/admin/model_descriptions.html b/src/registrar/templates/admin/model_descriptions.html index 6469f6188..3236ce57a 100644 --- a/src/registrar/templates/admin/model_descriptions.html +++ b/src/registrar/templates/admin/model_descriptions.html @@ -1,202 +1,35 @@
{% if opts.model_name == 'domainrequest' %} -

- This table contains all domain requests that have been started within the registrar and the status of those requests. - Updating values here will immediately update the corresponding values that users see in the registrar. -

- -

- Once a domain request has been adjudicated, the details of that request should not be modified. - To update attributes (like an organization’s name) after a domain’s approval, - go to Domains. - Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request. -

+ {% include "django/admin/includes/descriptions/domain_request_description.html" %} {% elif opts.model_name == 'domaininformation' %} -

- Domain information represents the basic metadata for an approved domain and the organization that manages it. - It does not include any information specific to the registry (DNS name servers, security email). - Registry-related information - can be managed within the Domains table. -

- -

- Updating values here will immediately update the corresponding values that users see in the registrar. -

- -

- Domain information is similar to Domain requests, - and the fields are nearly identical, - but edits made to one are not made to the other. - Domain information exists so we don’t modify details of an approved request after adjudication - (since a domain request should be maintained as-adjudicated for records retention purposes). - Entries are created here upon approval of a domain request. -

+ {% include "django/admin/includes/descriptions/domain_information_description.html" %} {% elif opts.model_name == 'domaininvitation' %} -

- Domain invitations contain all individuals who have been invited to manage a .gov domain. - Invitations are sent via email, and the recipient must log in to the registrar to officially - accept and become a domain manager. -

- -

- An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. - A “received” status indicates that the recipient has logged in. -

- -

- If an invitation is created in this table, an email will not be sent. - To have an email sent, go to the domain in Domains, - click the “Manage domain” button, and add a domain manager. -

+ {% include "django/admin/includes/descriptions/domain_invitation_description.html" %} {% elif opts.model_name == 'contact' %} -

- Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, - including other employees and authorizing officials. - Only contacts who have access to the registrar will have - a corresponding record within the Users table. - Updating someone’s contact information here will not affect that person’s Login.gov information. -

+ {% include "django/admin/includes/descriptions/contact_description.html" %} {% elif opts.model_name == 'logentry' %} -

- Log entries represent instances where something was created, updated, or deleted within the registrar or /admin. - The table on this page is useful for searching actions across all records. - To understand what happened to an individual record (like a domain request), - it’s better to go to that specific page - (Domain requests) and click on the “History” button. -

+ {% include "django/admin/includes/descriptions/logentry_description.html" %} {% elif opts.model_name == 'domain'%} -

- This table contains all approved domains in the .gov registrar. - In other words, with the exception of domains in the “Unknown”, ”On hold”, and “Deleted” states, - this table matches the list of domains in the .gov zone file. -

- -

- Individual domain pages allow you to view registry-related details and edit the associated domain information. -

- -

- Other actions available on the domain page include the ability to: -

-
    -
  • Extend a domain’s expiration date
  • -
  • Place a domain “On hold”
  • -
  • Remove a domain from the registry
  • -
  • View the domain from a domain manager’s perspective -
- -

- To view a domain from a domain manager’s perspective, click the "Manage domain" button. - That will allow you to make changes (e.g., update DNS settings, invite people to manage the domain) - directly inside the registrar. -

+ {% include "django/admin/includes/descriptions/domain_description.html" %} {% elif opts.model_name == 'draftdomain' %} -

- This table represents all “requested domains” that have been saved within a domain request form. - If a registrant changes the requested domain in their form, - the original name they listed and the new name will appear as separate records. -

- -

- This table does not include “alternative domains,” - which are housed in the Websites table. -

+ {% include "django/admin/includes/descriptions/draft_domain_description.html" %} {% elif opts.model_name == 'host' %} -

- Entries in the Hosts table indicate the relationship between an approved domain and a name server address - (and, if applicable, the IP address for a name server address). -

- -

- In general, you should not modify these values here. They should be updated directly inside the registrar. - To update a domain’s name servers and/or IP addresses, - in the registrar, go to the domain in Domains, - then click the "Manage domain" button. -

+ {% include "django/admin/includes/descriptions/host_description.html" %} {% elif opts.model_name == 'publiccontact' %} -

- Public contacts represent the three registry contact types (administrative, technical, and security) - and their fields that are exposed in WHOIS data. -

- -

- We don’t currently allow registrants to publish real contact information to WHOIS, - but we must publish something to WHOIS. For each of the contact types, we use default values that are - associated with the program instead of the real contact information, - which we then redact in whole at the registry/WHOIS. - We do allow registrants to set a security contact email address, - which is published to WHOIS when a user sets one. -

- -

- The public contacts in this table are a reflection of the data in the registry and should not be updated. - This information is primarily used by developers for validation purposes. -

+ {% include "django/admin/includes/descriptions/public_contact_description.html" %} {% elif opts.model_name == 'transitiondomain' %} -

- This table represents the domains that were transitioned from the old registry in November 2023. - This data has been preserved for historical reference and should not be updated. -

+ {% include "django/admin/includes/descriptions/transition_domain_description.html" %} {% elif opts.model_name == 'userdomainrole' %} -

- This table represents the managers who are assigned to each domain in the registrar. - There are separate records for each domain/manager combination. - Managers can update information related to a domain, such as DNS data and security contact. -

- -

- The creator of an approved domain request automatically becomes a manager for that domain. - Anyone who retrieves a domain invitation is also assigned the manager role. -

+ {% include "django/admin/includes/descriptions/user_domain_role_description.html" %} {% elif opts.model_name == 'usergroup' %} -

- Groups are a way to bundle admin permissions so they can be easily assigned to multiple users. - Once a group is created, it can be assigned to people via the Users table. -

- -

- To maintain a single source of truth across all environments (stable, staging, etc.), - the groups and permissions are set and updated through the codebase. - Do not add, edit or delete user groups here. -

+ {% include "django/admin/includes/descriptions/user_group_description.html" %} {% elif opts.model_name == 'user' %} -

- A user is anyone who has access to the registrar. This includes request creators, domain managers, and CISA administrators. - Within each user record, you can review that user’s permissions and user status. -

- -

- If a user has a domain request in ineligible status, then their user status will be “restricted.” - Users who are in restricted status cannot create/edit domain requests or approved domains, - and their existing requests are locked. They will see a 403 error if they try to take action in the registrar. -

- -

- Each user record displays the associated “contact” info for that user, - which is the same info found in the Contacts table. - Updating these details on the user record will also update the corresponding contact record for that user. -

+ {% include "django/admin/includes/descriptions/user_description.html" %} {% elif opts.model_name == 'verifiedbystaff' %} -

- This table contains users who have been allowed to bypass identity proofing through Login.gov after approval by a CISA representative. - Bypassing identity proofing means they will not be asked to provide a form of ID, PII, and so on. -

- -

- Additions to this table should be rare, and only after obtaining confirmation of their identity directly (or from a trusted person). - Once a verified-by-staff user has been added as a domain manager, they can be removed from this list, - (However, if they are removed as a domain manager for all domains and they attempt to sign in again, they will be identity proofed by Login.gov). -

+ {% include "django/admin/includes/descriptions/verified_by_staff_description.html" %} {% elif opts.model_name == 'website' %} -

- This table lists all the “current websites” and “alternative domains” that users have submitted in domain requests since January 2024. -

- -

- This does not include any “requested domains” that have appeared within the Domain requests table. - Those names are managed in the Draft domains table. -

+ {% include "django/admin/includes/descriptions/website.html" %} {% else %}

This table does not have a description yet.

{% endif %} diff --git a/src/registrar/templates/django/admin/includes/descriptions/contact_description.html b/src/registrar/templates/django/admin/includes/descriptions/contact_description.html new file mode 100644 index 000000000..c264f238f --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/contact_description.html @@ -0,0 +1,7 @@ +

+Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, +including other employees and authorizing officials. +Only contacts who have access to the registrar will have +a corresponding record within the Users table. +Updating someone’s contact information here will not affect that person’s Login.gov information. +

\ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_description.html new file mode 100644 index 000000000..c6e4a0fa6 --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_description.html @@ -0,0 +1,25 @@ +

+This table contains all approved domains in the .gov registrar. +In other words, with the exception of domains in the “Unknown”, ”On hold”, and “Deleted” states, +this table matches the list of domains in the .gov zone file. +

+ +

+Individual domain pages allow you to view registry-related details and edit the associated domain information. +

+ +

+Other actions available on the domain page include the ability to: +

+
    +
  • Extend a domain’s expiration date
  • +
  • Place a domain “On hold”
  • +
  • Remove a domain from the registry
  • +
  • View the domain from a domain manager’s perspective +
+ +

+To view a domain from a domain manager’s perspective, click the "Manage domain" button. +That will allow you to make changes (e.g., update DNS settings, invite people to manage the domain) +directly inside the registrar. +

\ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_information_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_information_description.html new file mode 100644 index 000000000..ccf683e59 --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_information_description.html @@ -0,0 +1,19 @@ +

+Domain information represents the basic metadata for an approved domain and the organization that manages it. +It does not include any information specific to the registry (DNS name servers, security email). +Registry-related information +can be managed within the Domains table. +

+ +

+Updating values here will immediately update the corresponding values that users see in the registrar. +

+ +

+Domain information is similar to Domain requests, +and the fields are nearly identical, +but edits made to one are not made to the other. +Domain information exists so we don’t modify details of an approved request after adjudication +(since a domain request should be maintained as-adjudicated for records retention purposes). +Entries are created here upon approval of a domain request. +

diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html new file mode 100644 index 000000000..495c64eb4 --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html @@ -0,0 +1,16 @@ +

+Domain invitations contain all individuals who have been invited to manage a .gov domain. +Invitations are sent via email, and the recipient must log in to the registrar to officially +accept and become a domain manager. +

+ +

+An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. +A “received” status indicates that the recipient has logged in. +

+ +

+If an invitation is created in this table, an email will not be sent. +To have an email sent, go to the domain in Domains, +click the “Manage domain” button, and add a domain manager. +

\ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_request_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_request_description.html new file mode 100644 index 000000000..03cb2a0fa --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_request_description.html @@ -0,0 +1,11 @@ +

+This table contains all domain requests that have been started within the registrar and the status of those requests. +Updating values here will immediately update the corresponding values that users see in the registrar. +

+ +

+Once a domain request has been adjudicated, the details of that request should not be modified. +To update attributes (like an organization’s name) after a domain’s approval, +go to Domains. +Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request. +

diff --git a/src/registrar/templates/django/admin/includes/descriptions/draft_domain_description.html b/src/registrar/templates/django/admin/includes/descriptions/draft_domain_description.html new file mode 100644 index 000000000..818b74b3b --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/draft_domain_description.html @@ -0,0 +1,10 @@ +

+This table represents all “requested domains” that have been saved within a domain request form. +If a registrant changes the requested domain in their form, +the original name they listed and the new name will appear as separate records. +

+ +

+This table does not include “alternative domains,” +which are housed in the Websites table. +

\ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/descriptions/host_description.html b/src/registrar/templates/django/admin/includes/descriptions/host_description.html new file mode 100644 index 000000000..99f7968fb --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/host_description.html @@ -0,0 +1,11 @@ +

+Entries in the Hosts table indicate the relationship between an approved domain and a name server address +(and, if applicable, the IP address for a name server address). +

+ +

+In general, you should not modify these values here. They should be updated directly inside the registrar. +To update a domain’s name servers and/or IP addresses, +in the registrar, go to the domain in Domains, +then click the "Manage domain" button. +

\ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/descriptions/logentry_description.html b/src/registrar/templates/django/admin/includes/descriptions/logentry_description.html new file mode 100644 index 000000000..29185f27b --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/logentry_description.html @@ -0,0 +1,7 @@ +

+Log entries represent instances where something was created, updated, or deleted within the registrar or /admin. +The table on this page is useful for searching actions across all records. +To understand what happened to an individual record (like a domain request), +it’s better to go to that specific page +(Domain requests) and click on the “History” button. +

\ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/descriptions/public_contact_description.html b/src/registrar/templates/django/admin/includes/descriptions/public_contact_description.html new file mode 100644 index 000000000..beab93929 --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/public_contact_description.html @@ -0,0 +1,18 @@ +

+Public contacts represent the three registry contact types (administrative, technical, and security) +and their fields that are exposed in WHOIS data. +

+ +

+We don’t currently allow registrants to publish real contact information to WHOIS, +but we must publish something to WHOIS. For each of the contact types, we use default values that are +associated with the program instead of the real contact information, +which we then redact in whole at the registry/WHOIS. +We do allow registrants to set a security contact email address, +which is published to WHOIS when a user sets one. +

+ +

+The public contacts in this table are a reflection of the data in the registry and should not be updated. +This information is primarily used by developers for validation purposes. +

\ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/descriptions/transition_domain_description.html b/src/registrar/templates/django/admin/includes/descriptions/transition_domain_description.html new file mode 100644 index 000000000..6eb450015 --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/transition_domain_description.html @@ -0,0 +1,4 @@ +

+This table represents the domains that were transitioned from the old registry in November 2023. +This data has been preserved for historical reference and should not be updated. +

\ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/descriptions/user_description.html b/src/registrar/templates/django/admin/includes/descriptions/user_description.html new file mode 100644 index 000000000..d50c210a1 --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/user_description.html @@ -0,0 +1,16 @@ +

+A user is anyone who has access to the registrar. This includes request creators, domain managers, and CISA administrators. +Within each user record, you can review that user’s permissions and user status. +

+ +

+If a user has a domain request in ineligible status, then their user status will be “restricted.” +Users who are in restricted status cannot create/edit domain requests or approved domains, +and their existing requests are locked. They will see a 403 error if they try to take action in the registrar. +

+ +

+Each user record displays the associated “contact” info for that user, +which is the same info found in the Contacts table. +Updating these details on the user record will also update the corresponding contact record for that user. +

\ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/descriptions/user_domain_role_description.html b/src/registrar/templates/django/admin/includes/descriptions/user_domain_role_description.html new file mode 100644 index 000000000..45f45957b --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/user_domain_role_description.html @@ -0,0 +1,10 @@ +

+This table represents the managers who are assigned to each domain in the registrar. +There are separate records for each domain/manager combination. +Managers can update information related to a domain, such as DNS data and security contact. +

+ +

+The creator of an approved domain request automatically becomes a manager for that domain. +Anyone who retrieves a domain invitation is also assigned the manager role. +

\ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/descriptions/user_group_description.html b/src/registrar/templates/django/admin/includes/descriptions/user_group_description.html new file mode 100644 index 000000000..22a692a8a --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/user_group_description.html @@ -0,0 +1,10 @@ +

+Groups are a way to bundle admin permissions so they can be easily assigned to multiple users. +Once a group is created, it can be assigned to people via the Users table. +

+ +

+To maintain a single source of truth across all environments (stable, staging, etc.), +the groups and permissions are set and updated through the codebase. +Do not add, edit or delete user groups here. +

\ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/descriptions/verified_by_staff_description.html b/src/registrar/templates/django/admin/includes/descriptions/verified_by_staff_description.html new file mode 100644 index 000000000..3f2580ec8 --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/verified_by_staff_description.html @@ -0,0 +1,10 @@ +

+This table contains users who have been allowed to bypass identity proofing through Login.gov after approval by a CISA representative. +Bypassing identity proofing means they will not be asked to provide a form of ID, PII, and so on. +

+ +

+Additions to this table should be rare, and only after obtaining confirmation of their identity directly (or from a trusted person). +Once a verified-by-staff user has been added as a domain manager, they can be removed from this list, +(However, if they are removed as a domain manager for all domains and they attempt to sign in again, they will be identity proofed by Login.gov). +

\ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/descriptions/website.html b/src/registrar/templates/django/admin/includes/descriptions/website.html new file mode 100644 index 000000000..1e48a1495 --- /dev/null +++ b/src/registrar/templates/django/admin/includes/descriptions/website.html @@ -0,0 +1,8 @@ +

+This table lists all the “current websites” and “alternative domains” that users have submitted in domain requests since January 2024. +

+ +

+This does not include any “requested domains” that have appeared within the Domain requests table. +Those names are managed in the Draft domains table. +

\ No newline at end of file From 2834134ebe1887b973b9b0f588c2cf15b826b930 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 3 May 2024 08:33:47 -0600 Subject: [PATCH 64/76] Cleanup --- src/registrar/templates/admin/model_descriptions.html | 2 +- .../descriptions/{website.html => website_description.html} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/registrar/templates/django/admin/includes/descriptions/{website.html => website_description.html} (100%) diff --git a/src/registrar/templates/admin/model_descriptions.html b/src/registrar/templates/admin/model_descriptions.html index 3236ce57a..c075e03a5 100644 --- a/src/registrar/templates/admin/model_descriptions.html +++ b/src/registrar/templates/admin/model_descriptions.html @@ -29,7 +29,7 @@ {% elif opts.model_name == 'verifiedbystaff' %} {% include "django/admin/includes/descriptions/verified_by_staff_description.html" %} {% elif opts.model_name == 'website' %} - {% include "django/admin/includes/descriptions/website.html" %} + {% include "django/admin/includes/descriptions/website_description.html" %} {% else %}

This table does not have a description yet.

{% endif %} diff --git a/src/registrar/templates/django/admin/includes/descriptions/website.html b/src/registrar/templates/django/admin/includes/descriptions/website_description.html similarity index 100% rename from src/registrar/templates/django/admin/includes/descriptions/website.html rename to src/registrar/templates/django/admin/includes/descriptions/website_description.html From 55306f6d2d968fc956eedc6d6f0801d2893f90fe Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 3 May 2024 08:43:50 -0600 Subject: [PATCH 65/76] Fix tests --- src/registrar/tests/test_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index a362a5231..3b1e9f2d3 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -537,7 +537,7 @@ class TestDomainAdmin(MockEppLib, WebTest): # There are 4 template references to Federal (4) plus four references in the table # for our actual domain_request - self.assertContains(response, "Federal", count=39) + self.assertContains(response, "Federal", count=42) # This may be a bit more robust self.assertContains(response, 'Federal', count=1) # Now let's make sure the long description does not exist @@ -1310,7 +1310,7 @@ class TestDomainRequestAdmin(MockEppLib): response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal") # There are 2 template references to Federal (4) and two in the results data # of the request - self.assertContains(response, "Federal", count=37) + self.assertContains(response, "Federal", count=40) # This may be a bit more robust self.assertContains(response, 'Federal', count=1) # Now let's make sure the long description does not exist From 59f9d6bfecac53b36a4058148ec569a798d5e775 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 3 May 2024 10:21:43 -0600 Subject: [PATCH 66/76] PR suggestions --- src/registrar/assets/sass/_theme/_admin.scss | 6 +++++- .../admin/includes/descriptions/contact_description.html | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index c1f6571b9..b4e7c0cb2 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -685,7 +685,6 @@ div.dja__model-description{ p, li { font-size: medium; - line-height: 1.2em; } @media (prefers-color-scheme: light) { @@ -702,6 +701,7 @@ div.dja__model-description{ li { list-style-type: disc; + font-family: Source Sans Pro Web,Helvetica Neue,Helvetica,Roboto,Arial,sans-serif; } &.dja__model-description--no-overflow { @@ -710,3 +710,7 @@ div.dja__model-description{ } } + +.text-underline { + text-decoration: underline !important; +} diff --git a/src/registrar/templates/django/admin/includes/descriptions/contact_description.html b/src/registrar/templates/django/admin/includes/descriptions/contact_description.html index c264f238f..18fb14568 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/contact_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/contact_description.html @@ -3,5 +3,8 @@ Contacts include anyone who has access to the registrar (known as “users”) a including other employees and authorizing officials. Only contacts who have access to the registrar will have a corresponding record within the
Users table. +

+ +

Updating someone’s contact information here will not affect that person’s Login.gov information.

\ No newline at end of file From 63f8c865d28df8c23ff36c442a722ff1f0a1f824 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 3 May 2024 13:34:57 -0400 Subject: [PATCH 67/76] Show more/less for status auditlogs --- src/registrar/admin.py | 8 +-- src/registrar/assets/js/dja-collapse.js | 39 ++++++++++++-- src/registrar/assets/sass/_theme/_admin.scss | 44 ++++++++++------ src/registrar/templates/admin/fieldset.html | 4 +- .../admin/includes/detail_table_fieldset.html | 52 +++++++++++-------- 5 files changed, 98 insertions(+), 49 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 485751b3c..51ceefba6 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1013,7 +1013,7 @@ class DomainInformationAdmin(ListHeaderAdmin): ( "Show details", { - "classes": ["collapse--dotgov"], + "classes": ["collapse--dgfieldset"], "description": "Extends type of organization", "fields": [ "federal_type", @@ -1039,7 +1039,7 @@ class DomainInformationAdmin(ListHeaderAdmin): ( "Show details", { - "classes": ["collapse--dotgov"], + "classes": ["collapse--dgfieldset"], "description": "Extends organization name and mailing address", "fields": [ "address_line1", @@ -1266,7 +1266,7 @@ class DomainRequestAdmin(ListHeaderAdmin): ( "Show details", { - "classes": ["collapse--dotgov"], + "classes": ["collapse--dgfieldset"], "description": "Extends type of organization", "fields": [ "federal_type", @@ -1292,7 +1292,7 @@ class DomainRequestAdmin(ListHeaderAdmin): ( "Show details", { - "classes": ["collapse--dotgov"], + "classes": ["collapse--dgfieldset"], "description": "Extends organization name and mailing address", "fields": [ "address_line1", diff --git a/src/registrar/assets/js/dja-collapse.js b/src/registrar/assets/js/dja-collapse.js index c33954192..6fd838e2e 100644 --- a/src/registrar/assets/js/dja-collapse.js +++ b/src/registrar/assets/js/dja-collapse.js @@ -4,19 +4,18 @@ * Works with our fieldset override */ -/*global gettext*/ 'use strict'; { window.addEventListener('load', function() { // Add anchor tag for Show/Hide link - const fieldsets = document.querySelectorAll('fieldset.collapse--dotgov'); + const fieldsets = document.querySelectorAll('fieldset.collapse--dgfieldset'); for (const [i, elem] of fieldsets.entries()) { // Don't hide if fields in this fieldset have errors if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) { elem.classList.add('collapsed'); const button = elem.querySelector('button'); button.id = 'fieldsetcollapser' + i; - button.className = 'collapse-toggle--dotgov usa-button usa-button--unstyled'; + button.className = 'collapse-toggle--dgfieldset usa-button usa-button--unstyled'; } } // Add toggle to hide/show anchor tag @@ -38,8 +37,40 @@ fieldset.classList.add('collapsed'); } }; - document.querySelectorAll('.collapse-toggle--dotgov').forEach(function(el) { + document.querySelectorAll('.collapse-toggle--dgfieldset').forEach(function(el) { el.addEventListener('click', toggleFuncDotgov); }); }); } + +'use strict'; +{ + window.addEventListener('load', function() { + // Add anchor tag for Show/Hide link + const collapsibleContent = document.querySelectorAll('fieldset.collapse--dgsimple'); + for (const [i, elem] of collapsibleContent.entries()) { + const button = elem.closest('div').querySelector('button'); + button.id = 'simplecollapser' + i; + } + // Add toggle to hide/show anchor tag + const toggleFuncDotgovSimple = function(e) { + const fieldset = this.closest('div').querySelector('.collapse--dgsimple'); + const spanElement = this.querySelector('span'); + const useElement = this.querySelector('use'); + if (fieldset.classList.contains('collapsed')) { + // Show + spanElement.textContent = 'Hide details'; + useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less'); + fieldset.classList.remove('collapsed'); + } else { + // Hide + spanElement.textContent = 'Show details'; + useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more'); + fieldset.classList.add('collapsed'); + } + }; + document.querySelectorAll('.collapse-toggle--dgsimple').forEach(function(el) { + el.addEventListener('click', toggleFuncDotgovSimple); + }); + }); +} diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index b4b590acb..8706af3b5 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -243,7 +243,7 @@ div#content > h2 { // in the future .object-tools li a, .object-tools p a { - font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; + font-family: family('sans'); text-transform: none!important; font-size: 14px!important; } @@ -293,7 +293,7 @@ div#content > h2 { .messagelist_content-list--unstyled { padding-left: 0; li { - font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; + font-family: family('sans'); font-size: 13.92px!important; background: none!important; padding: 0!important; @@ -542,32 +542,42 @@ address.dja-address-contact-list { } // Collapse button styles for fieldsets -.module.collapse--dotgov { +.module.collapse--dgfieldset { margin-top: -35px; padding-top: 0; border: none; - button { - background: none; - text-transform: none; +} +.collapse-toggle--dgsimple, +.module.collapse--dgfieldset button { + background: none; + text-transform: none; + color: var(--link-fg); + margin-top: 8px; + margin-left: 10px; + span { + text-decoration: underline; + font-size: 13px; + font-feature-settings: "kern"; + font-kerning: normal; + line-height: 13px; + font-family: family('sans'); + } + &:hover { color: var(--link-fg); - margin-top: 8px; - margin-left: 10px; - span { - text-decoration: underline; - font-size: 13px; - font-feature-settings: "kern"; - font-kerning: normal; - line-height: 13px; - font-family: -apple-system, "system-ui", "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + svg { + color: var(--link-fg); } } } -.collapse--dotgov.collapsed .collapse-toggle--dotgov { +.collapse--dgfieldset.collapsed .collapse-toggle--dgfieldset { display: inline-block!important; * { display: inline-block; } } +.collapse--dgsimple.collapsed { + display: none; +} .dja-status-list { border-top: solid 1px var(--border-color); @@ -576,7 +586,7 @@ address.dja-address-contact-list { padding-top: 10px; li { line-height: 1.5; - font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif !important; + font-family: family('sans'); padding-top: 0; padding-bottom: 0; } diff --git a/src/registrar/templates/admin/fieldset.html b/src/registrar/templates/admin/fieldset.html index 19c5db294..89537b098 100644 --- a/src/registrar/templates/admin/fieldset.html +++ b/src/registrar/templates/admin/fieldset.html @@ -8,7 +8,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
{% if fieldset.name %} {# Customize the markup for the collapse toggle #} - {% if 'collapse--dotgov' in fieldset.classes %} + {% if 'collapse--dgfieldset' in fieldset.classes %}
{% elif field.field.name == "creator" %} From 164940e7435d6e249daaa07cfc8ad1ffac8b9797 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 3 May 2024 11:48:27 -0600 Subject: [PATCH 68/76] Rachid PR suggestions --- src/registrar/assets/sass/_theme/_admin.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index b4e7c0cb2..13850b6f6 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -695,7 +695,7 @@ div.dja__model-description{ @media (prefers-color-scheme: dark) { p, li { - color: var(--body-bg); + color: var(--secondary); } } @@ -704,6 +704,10 @@ div.dja__model-description{ font-family: Source Sans Pro Web,Helvetica Neue,Helvetica,Roboto,Arial,sans-serif; } + a { + font-size: medium; + } + &.dja__model-description--no-overflow { display: block; overflow: auto; From 31249457b714620be8f36ad7ac31ae8de3164347 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 3 May 2024 13:40:32 -0600 Subject: [PATCH 69/76] Add trailing lines --- .../django/admin/includes/descriptions/contact_description.html | 2 +- .../django/admin/includes/descriptions/domain_description.html | 2 +- .../includes/descriptions/domain_invitation_description.html | 2 +- .../admin/includes/descriptions/draft_domain_description.html | 2 +- .../django/admin/includes/descriptions/host_description.html | 2 +- .../admin/includes/descriptions/logentry_description.html | 2 +- .../admin/includes/descriptions/public_contact_description.html | 2 +- .../includes/descriptions/transition_domain_description.html | 2 +- .../django/admin/includes/descriptions/user_description.html | 2 +- .../includes/descriptions/user_domain_role_description.html | 2 +- .../admin/includes/descriptions/user_group_description.html | 2 +- .../includes/descriptions/verified_by_staff_description.html | 2 +- .../django/admin/includes/descriptions/website_description.html | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/registrar/templates/django/admin/includes/descriptions/contact_description.html b/src/registrar/templates/django/admin/includes/descriptions/contact_description.html index 18fb14568..ff213a7d1 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/contact_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/contact_description.html @@ -7,4 +7,4 @@ a corresponding record within the \ No newline at end of file +

diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html index 495c64eb4..68876affd 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html @@ -13,4 +13,4 @@ A “received” status indicates that the recipient has logged in. If an invitation is created in this table, an email will not be sent. To have an email sent, go to the domain in
Domains, click the “Manage domain” button, and add a domain manager. -

\ No newline at end of file +

diff --git a/src/registrar/templates/django/admin/includes/descriptions/draft_domain_description.html b/src/registrar/templates/django/admin/includes/descriptions/draft_domain_description.html index 818b74b3b..687431201 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/draft_domain_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/draft_domain_description.html @@ -7,4 +7,4 @@ the original name they listed and the new name will appear as separate records.

This table does not include “alternative domains,” which are housed in the Websites table. -

\ No newline at end of file +

diff --git a/src/registrar/templates/django/admin/includes/descriptions/host_description.html b/src/registrar/templates/django/admin/includes/descriptions/host_description.html index 99f7968fb..778c2f812 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/host_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/host_description.html @@ -8,4 +8,4 @@ In general, you should not modify these values here. They should be updated dire To update a domain’s name servers and/or IP addresses, in the registrar, go to the domain in Domains, then click the "Manage domain" button. -

\ No newline at end of file +

diff --git a/src/registrar/templates/django/admin/includes/descriptions/logentry_description.html b/src/registrar/templates/django/admin/includes/descriptions/logentry_description.html index 29185f27b..deb3876ca 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/logentry_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/logentry_description.html @@ -4,4 +4,4 @@ The table on this page is useful for searching actions across all records. To understand what happened to an individual record (like a domain request), it’s better to go to that specific page (Domain requests) and click on the “History” button. -

\ No newline at end of file +

diff --git a/src/registrar/templates/django/admin/includes/descriptions/public_contact_description.html b/src/registrar/templates/django/admin/includes/descriptions/public_contact_description.html index beab93929..809b62a33 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/public_contact_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/public_contact_description.html @@ -15,4 +15,4 @@ which is published to WHOIS when a user sets one.

The public contacts in this table are a reflection of the data in the registry and should not be updated. This information is primarily used by developers for validation purposes. -

\ No newline at end of file +

diff --git a/src/registrar/templates/django/admin/includes/descriptions/transition_domain_description.html b/src/registrar/templates/django/admin/includes/descriptions/transition_domain_description.html index 6eb450015..331c7b18b 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/transition_domain_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/transition_domain_description.html @@ -1,4 +1,4 @@

This table represents the domains that were transitioned from the old registry in November 2023. This data has been preserved for historical reference and should not be updated. -

\ No newline at end of file +

diff --git a/src/registrar/templates/django/admin/includes/descriptions/user_description.html b/src/registrar/templates/django/admin/includes/descriptions/user_description.html index d50c210a1..2a68c0def 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/user_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/user_description.html @@ -13,4 +13,4 @@ and their existing requests are locked. They will see a 403 error if they try t Each user record displays the associated “contact” info for that user, which is the same info found in the Contacts table. Updating these details on the user record will also update the corresponding contact record for that user. -

\ No newline at end of file +

diff --git a/src/registrar/templates/django/admin/includes/descriptions/user_domain_role_description.html b/src/registrar/templates/django/admin/includes/descriptions/user_domain_role_description.html index 45f45957b..7066fcb93 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/user_domain_role_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/user_domain_role_description.html @@ -7,4 +7,4 @@ Managers can update information related to a domain, such as DNS data and securi

The creator of an approved domain request automatically becomes a manager for that domain. Anyone who retrieves a domain invitation is also assigned the manager role. -

\ No newline at end of file +

diff --git a/src/registrar/templates/django/admin/includes/descriptions/user_group_description.html b/src/registrar/templates/django/admin/includes/descriptions/user_group_description.html index 22a692a8a..cf13fbf20 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/user_group_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/user_group_description.html @@ -7,4 +7,4 @@ Once a group is created, it can be assigned to people via the Domain requests table. Those names are managed in the Draft domains table. -

\ No newline at end of file +

From 9ec10424206d9a7acf1e81eb2854d56c612c545b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 3 May 2024 13:55:27 -0600 Subject: [PATCH 70/76] Update _admin.scss --- src/registrar/assets/sass/_theme/_admin.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 13850b6f6..03082fb8e 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -706,6 +706,7 @@ div.dja__model-description{ a { font-size: medium; + color: #005288 !important; } &.dja__model-description--no-overflow { From d691e6f5e363f08edd2aa7b24e7af8eff71fd9c7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 3 May 2024 14:08:25 -0600 Subject: [PATCH 71/76] Fix links --- src/registrar/assets/sass/_theme/_admin.scss | 15 ++------------- .../descriptions/contact_description.html | 2 +- .../domain_information_description.html | 4 ++-- .../domain_invitation_description.html | 2 +- .../descriptions/domain_request_description.html | 2 +- .../descriptions/draft_domain_description.html | 2 +- .../includes/descriptions/host_description.html | 2 +- .../descriptions/logentry_description.html | 2 +- .../includes/descriptions/user_description.html | 2 +- .../descriptions/user_group_description.html | 2 +- .../descriptions/website_description.html | 4 ++-- 11 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 03082fb8e..b4b5c657d 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -685,18 +685,7 @@ div.dja__model-description{ p, li { font-size: medium; - } - - @media (prefers-color-scheme: light) { - p, li { - color: var(--body-fg); - } - } - - @media (prefers-color-scheme: dark) { - p, li { - color: var(--secondary); - } + color: var(--secondary); } li { @@ -704,7 +693,7 @@ div.dja__model-description{ font-family: Source Sans Pro Web,Helvetica Neue,Helvetica,Roboto,Arial,sans-serif; } - a { + a, a:link, a:visited { font-size: medium; color: #005288 !important; } diff --git a/src/registrar/templates/django/admin/includes/descriptions/contact_description.html b/src/registrar/templates/django/admin/includes/descriptions/contact_description.html index ff213a7d1..11141dca8 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/contact_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/contact_description.html @@ -2,7 +2,7 @@ Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, including other employees and authorizing officials. Only contacts who have access to the registrar will have -a corresponding record within the
Users table. +a corresponding record within the Users table.

diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_information_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_information_description.html index ccf683e59..36d197cf8 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_information_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_information_description.html @@ -2,7 +2,7 @@ Domain information represents the basic metadata for an approved domain and the organization that manages it. It does not include any information specific to the registry (DNS name servers, security email). Registry-related information -can be managed within the Domains table. +can be managed within the Domains table.

@@ -10,7 +10,7 @@ Updating values here will immediately update the corresponding values that users

-Domain information is similar to Domain requests, +Domain information is similar to Domain requests, and the fields are nearly identical, but edits made to one are not made to the other. Domain information exists so we don’t modify details of an approved request after adjudication diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html index 68876affd..7765b9203 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html @@ -11,6 +11,6 @@ A “received” status indicates that the recipient has logged in.

If an invitation is created in this table, an email will not be sent. -To have an email sent, go to the domain in Domains, +To have an email sent, go to the domain in Domains, click the “Manage domain” button, and add a domain manager.

diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_request_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_request_description.html index 03cb2a0fa..5adc07454 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_request_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_request_description.html @@ -6,6 +6,6 @@ Updating values here will immediately update the corresponding values that users

Once a domain request has been adjudicated, the details of that request should not be modified. To update attributes (like an organization’s name) after a domain’s approval, -go to Domains. +go to Domains. Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request.

diff --git a/src/registrar/templates/django/admin/includes/descriptions/draft_domain_description.html b/src/registrar/templates/django/admin/includes/descriptions/draft_domain_description.html index 687431201..9e0ac9914 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/draft_domain_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/draft_domain_description.html @@ -6,5 +6,5 @@ the original name they listed and the new name will appear as separate records.

This table does not include “alternative domains,” -which are housed in the Websites table. +which are housed in the Websites table.

diff --git a/src/registrar/templates/django/admin/includes/descriptions/host_description.html b/src/registrar/templates/django/admin/includes/descriptions/host_description.html index 778c2f812..a39519898 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/host_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/host_description.html @@ -6,6 +6,6 @@ Entries in the Hosts table indicate the relationship between an approved domain

In general, you should not modify these values here. They should be updated directly inside the registrar. To update a domain’s name servers and/or IP addresses, -in the registrar, go to the domain in Domains, +in the registrar, go to the domain in Domains, then click the "Manage domain" button.

diff --git a/src/registrar/templates/django/admin/includes/descriptions/logentry_description.html b/src/registrar/templates/django/admin/includes/descriptions/logentry_description.html index deb3876ca..0dc3fe94e 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/logentry_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/logentry_description.html @@ -3,5 +3,5 @@ Log entries represent instances where something was created, updated, or deleted The table on this page is useful for searching actions across all records. To understand what happened to an individual record (like a domain request), it’s better to go to that specific page -(Domain requests) and click on the “History” button. +(Domain requests) and click on the “History” button.

diff --git a/src/registrar/templates/django/admin/includes/descriptions/user_description.html b/src/registrar/templates/django/admin/includes/descriptions/user_description.html index 2a68c0def..2f1777169 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/user_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/user_description.html @@ -11,6 +11,6 @@ and their existing requests are locked. They will see a 403 error if they try t

Each user record displays the associated “contact” info for that user, -which is the same info found in the Contacts table. +which is the same info found in the Contacts table. Updating these details on the user record will also update the corresponding contact record for that user.

diff --git a/src/registrar/templates/django/admin/includes/descriptions/user_group_description.html b/src/registrar/templates/django/admin/includes/descriptions/user_group_description.html index cf13fbf20..610c8b430 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/user_group_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/user_group_description.html @@ -1,6 +1,6 @@

Groups are a way to bundle admin permissions so they can be easily assigned to multiple users. -Once a group is created, it can be assigned to people via the Users table. +Once a group is created, it can be assigned to people via the Users table.

diff --git a/src/registrar/templates/django/admin/includes/descriptions/website_description.html b/src/registrar/templates/django/admin/includes/descriptions/website_description.html index 522daabd7..f6f5bdd1c 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/website_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/website_description.html @@ -3,6 +3,6 @@ This table lists all the “current websites” and “alternative domains” th

-This does not include any “requested domains” that have appeared within the Domain requests table. -Those names are managed in the Draft domains table. +This does not include any “requested domains” that have appeared within the Domain requests table. +Those names are managed in the Draft domains table.

From ec70c28848a437afa2ab6d515b513c3309fc5839 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 6 May 2024 08:51:06 -0600 Subject: [PATCH 72/76] Return N/A on edgecase --- src/registrar/templatetags/custom_filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index ff350197c..798558355 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -130,6 +130,6 @@ def get_region(state): "OR": 10, "WA": 10, } - return regions.get(state.upper(), None) + return regions.get(state.upper(), "N/A") else: return None From 01656ba9d29dbfd1f8b817196f7d1957275d39f6 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 6 May 2024 09:26:37 -0600 Subject: [PATCH 73/76] Add unit test --- src/registrar/tests/test_admin.py | 66 +++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index a7ab0f86d..678aef031 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -104,6 +104,72 @@ class TestDomainAdmin(MockEppLib, WebTest): ) super().setUp() + @less_console_noise_decorator + def test_staff_can_see_cisa_region_federal(self): + """Tests if staff can see CISA Region: N/A""" + + # Create a fake domain request + _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + _domain_request.approve() + + domain = _domain_request.approved_domain + p = "userpass" + self.client.login(username="staffuser", password=p) + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + + # Test if the page has the right CISA region + expected_html = '
CISA region: N/A
' + # Remove whitespace from expected_html + expected_html = "".join(expected_html.split()) + + # Remove whitespace from response content + response_content = "".join(response.content.decode().split()) + + # Check if response contains expected_html + self.assertIn(expected_html, response_content) + + @less_console_noise_decorator + def test_staff_can_see_cisa_region_non_federal(self): + """Tests if staff can see the correct CISA region""" + + # Create a fake domain request. State will be NY (2). + _domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type="interstate" + ) + + _domain_request.approve() + + domain = _domain_request.approved_domain + + p = "userpass" + self.client.login(username="staffuser", password=p) + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + + # Test if the page has the right CISA region + expected_html = '
CISA region: 2
' + # Remove whitespace from expected_html + expected_html = "".join(expected_html.split()) + + # Remove whitespace from response content + response_content = "".join(response.content.decode().split()) + + # Check if response contains expected_html + self.assertIn(expected_html, response_content) + @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" From 7461b29168cab67aca309918ea4a7185fef22e54 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 6 May 2024 09:53:12 -0600 Subject: [PATCH 74/76] add tests for domain information --- src/registrar/tests/test_admin.py | 66 +++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 678aef031..afe6994db 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2840,6 +2840,72 @@ class TestDomainInformationAdmin(TestCase): Contact.objects.all().delete() User.objects.all().delete() + + @less_console_noise_decorator + def test_admin_can_see_cisa_region_federal(self): + """Tests if admins can see CISA Region: N/A""" + + # Create a fake domain request + _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + _domain_request.approve() + + domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get() + + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/domaininformation/{}/change/".format(domain_information.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain_information.domain.name) + + # Test if the page has the right CISA region + expected_html = '
CISA region: N/A
' + # Remove whitespace from expected_html + expected_html = "".join(expected_html.split()) + + # Remove whitespace from response content + response_content = "".join(response.content.decode().split()) + + # Check if response contains expected_html + self.assertIn(expected_html, response_content) + + @less_console_noise_decorator + def test_admin_can_see_cisa_region_non_federal(self): + """Tests if admins can see the correct CISA region""" + + # Create a fake domain request. State will be NY (2). + _domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type="interstate" + ) + _domain_request.approve() + + domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get() + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get( + "/admin/registrar/domaininformation/{}/change/".format(domain_information.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain_information.domain.name) + + # Test if the page has the right CISA region + expected_html = '
CISA region: 2
' + # Remove whitespace from expected_html + expected_html = "".join(expected_html.split()) + + # Remove whitespace from response content + response_content = "".join(response.content.decode().split()) + + # Check if response contains expected_html + self.assertIn(expected_html, response_content) + @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" From f9e7a8a09773a40b9e7ce28fc548c548d726d81d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 6 May 2024 09:58:47 -0600 Subject: [PATCH 75/76] Linting --- src/registrar/tests/test_admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index afe6994db..4c09f6472 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2840,7 +2840,6 @@ class TestDomainInformationAdmin(TestCase): Contact.objects.all().delete() User.objects.all().delete() - @less_console_noise_decorator def test_admin_can_see_cisa_region_federal(self): """Tests if admins can see CISA Region: N/A""" From 61ee55f5fdfd18a23e9db0a9ca4b9d37939a36f2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 6 May 2024 11:06:54 -0600 Subject: [PATCH 76/76] Fix styling --- src/registrar/assets/sass/_theme/_admin.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 9554f7acd..9f5ea7a97 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -395,7 +395,6 @@ details.dja-detail-table { border-top: none; border-bottom: none; } - } @@ -643,13 +642,16 @@ address.dja-address-contact-list { display: inline-flex; padding-top: 4px; line-height: 14px; - color: var(--link-fg); width: max-content; font-size: unset; text-decoration: none !important; } } +button.usa-button__clipboard { + color: var(--link-fg); +} + .no-outline-on-click:focus { outline: none !important; }