diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index 214cf6076..84f228893 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -25,6 +25,9 @@ jobs: || startsWith(github.head_ref, 'meoward/') || startsWith(github.head_ref, 'bob/') || startsWith(github.head_ref, 'cb/') + || startsWith(github.head_ref, 'hotgov/') + || startsWith(github.head_ref, 'litterbox/') + || startsWith(github.head_ref, 'ag/') outputs: environment: ${{ steps.var.outputs.environment}} runs-on: "ubuntu-latest" diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index 1967a6b92..81368f6e9 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -17,6 +17,8 @@ on: - staging - development - ag + - litterbox + - hotgov - cb - bob - meoward diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index 1277d3064..ad325c50a 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -17,6 +17,8 @@ on: - staging - development - ag + - litterbox + - hotgov - cb - bob - meoward diff --git a/docs/architecture/diagrams/models_diagram.md b/docs/architecture/diagrams/models_diagram.md index df0e39b7e..837906934 100644 --- a/docs/architecture/diagrams/models_diagram.md +++ b/docs/architecture/diagrams/models_diagram.md @@ -1,11 +1,15 @@ # Complete model documentation This is an auto-generated diagram of our data models generated with the -[django-model2puml](https://github.com/sen-den/django-model2puml) library -using the command +[django-model2puml](https://github.com/sen-den/django-model2puml) library. + +## How to generate the puml + +1. Uncomment `puml_generator` from `INSTALLED_APPS` in settings.py and docker-compose down and up +2. Run the following command to generate a puml file ```bash -$ docker compose app ./manage.py generate_puml --include registrar +docker compose exec app ./manage.py generate_puml --include registrar ``` ![Complete data models diagram](./models_diagram.svg) @@ -13,12 +17,19 @@ $ docker compose app ./manage.py generate_puml --include registrar
PlantUML source code -To regenerate this image using Docker, run +## How To regenerate the database svg image + +1. Copy your puml file contents into the bottom of this file and replace the current code marked by `plantuml` +2. Run the following command ```bash -$ docker run -v $(pwd):$(pwd) -w $(pwd) -it plantuml/plantuml -tsvg models_diagram.md +docker run -v $(pwd):$(pwd) -w $(pwd) -it plantuml/plantuml -tsvg models_diagram.md ``` +3. Remove the puml file from earlier (if you still have it) +4. Commit the new image and the md file + + ```plantuml @startuml class "registrar.Contact " as registrar.Contact #d6f4e9 { @@ -28,17 +39,97 @@ class "registrar.Contact " as registrar.Contact #d6f4e9 { + created_at (DateTimeField) + updated_at (DateTimeField) ~ user (OneToOneField) - + first_name (TextField) - + middle_name (TextField) - + last_name (TextField) - + title (TextField) - + email (TextField) + + first_name (CharField) + + middle_name (CharField) + + last_name (CharField) + + title (CharField) + + email (EmailField) + phone (PhoneNumberField) -- } registrar.Contact -- registrar.User +class "registrar.Host " as registrar.Host #d6f4e9 { + host + -- + + id (BigAutoField) + + created_at (DateTimeField) + + updated_at (DateTimeField) + + name (CharField) + ~ domain (ForeignKey) + -- +} +registrar.Host -- registrar.Domain + + +class "registrar.HostIP " as registrar.HostIP #d6f4e9 { + host ip + -- + + id (BigAutoField) + + created_at (DateTimeField) + + updated_at (DateTimeField) + + address (CharField) + ~ host (ForeignKey) + -- +} +registrar.HostIP -- registrar.Host + + +class "registrar.PublicContact " as registrar.PublicContact #d6f4e9 { + public contact + -- + + id (BigAutoField) + + created_at (DateTimeField) + + updated_at (DateTimeField) + + contact_type (CharField) + + registry_id (CharField) + ~ domain (ForeignKey) + + name (CharField) + + org (CharField) + + street1 (CharField) + + street2 (CharField) + + street3 (CharField) + + city (CharField) + + sp (CharField) + + pc (CharField) + + cc (CharField) + + email (EmailField) + + voice (CharField) + + fax (CharField) + + pw (CharField) + -- +} +registrar.PublicContact -- registrar.Domain + + +class "registrar.Domain " as registrar.Domain #d6f4e9 { + domain + -- + + id (BigAutoField) + + created_at (DateTimeField) + + updated_at (DateTimeField) + + name (DomainField) + + state (FSMField) + + expiration_date (DateField) + + security_contact_registry_id (TextField) + + deleted (DateField) + + first_ready (DateField) + -- +} + + +class "registrar.FederalAgency " as registrar.FederalAgency #d6f4e9 { + Federal agency + -- + + id (BigAutoField) + + created_at (DateTimeField) + + updated_at (DateTimeField) + + agency (CharField) + -- +} + + class "registrar.DomainRequest " as registrar.DomainRequest #d6f4e9 { domain request -- @@ -46,24 +137,25 @@ class "registrar.DomainRequest " as registrar.DomainRequest #d6f4e9 { + created_at (DateTimeField) + updated_at (DateTimeField) + status (FSMField) + + rejection_reason (TextField) + ~ federal_agency (ForeignKey) ~ creator (ForeignKey) ~ investigator (ForeignKey) + + generic_org_type (CharField) + + is_election_board (BooleanField) + organization_type (CharField) + federally_recognized_tribe (BooleanField) + state_recognized_tribe (BooleanField) - + tribe_name (TextField) - + federal_agency (TextField) + + tribe_name (CharField) + federal_type (CharField) - + is_election_board (BooleanField) - + organization_name (TextField) - + address_line1 (TextField) + + organization_name (CharField) + + address_line1 (CharField) + address_line2 (CharField) - + city (TextField) + + city (CharField) + state_territory (CharField) + zipcode (CharField) - + urbanization (TextField) - + type_of_work (TextField) - + more_organization_information (TextField) + + urbanization (CharField) + + about_your_organization (TextField) ~ authorizing_official (ForeignKey) ~ approved_domain (OneToOneField) ~ requested_domain (OneToOneField) @@ -71,17 +163,23 @@ class "registrar.DomainRequest " as registrar.DomainRequest #d6f4e9 { + purpose (TextField) + no_other_contacts_rationale (TextField) + anything_else (TextField) + + has_anything_else_text (BooleanField) + + cisa_representative_email (EmailField) + + has_cisa_representative (BooleanField) + is_policy_acknowledged (BooleanField) + + submission_date (DateField) + + notes (TextField) # current_websites (ManyToManyField) # alternative_domains (ManyToManyField) # other_contacts (ManyToManyField) -- } +registrar.DomainRequest -- registrar.FederalAgency registrar.DomainRequest -- registrar.User registrar.DomainRequest -- registrar.User registrar.DomainRequest -- registrar.Contact -registrar.DomainRequest -- registrar.DraftDomain registrar.DomainRequest -- registrar.Domain +registrar.DomainRequest -- registrar.DraftDomain registrar.DomainRequest -- registrar.Contact registrar.DomainRequest *--* registrar.Website registrar.DomainRequest *--* registrar.Website @@ -94,35 +192,37 @@ class "registrar.DomainInformation " as registrar.DomainInformation # + id (BigAutoField) + created_at (DateTimeField) + updated_at (DateTimeField) + ~ federal_agency (ForeignKey) ~ creator (ForeignKey) ~ domain_request (OneToOneField) + + generic_org_type (CharField) + organization_type (CharField) + federally_recognized_tribe (BooleanField) + state_recognized_tribe (BooleanField) - + tribe_name (TextField) - + federal_agency (TextField) + + tribe_name (CharField) + federal_type (CharField) + is_election_board (BooleanField) - + organization_name (TextField) - + address_line1 (TextField) + + organization_name (CharField) + + address_line1 (CharField) + address_line2 (CharField) - + city (TextField) + + city (CharField) + state_territory (CharField) + zipcode (CharField) - + urbanization (TextField) - + type_of_work (TextField) - + more_organization_information (TextField) + + urbanization (CharField) + + about_your_organization (TextField) ~ authorizing_official (ForeignKey) ~ domain (OneToOneField) ~ submitter (ForeignKey) + purpose (TextField) + no_other_contacts_rationale (TextField) + anything_else (TextField) + + cisa_representative_email (EmailField) + is_policy_acknowledged (BooleanField) - + security_email (EmailField) + + notes (TextField) # other_contacts (ManyToManyField) -- } +registrar.DomainInformation -- registrar.FederalAgency registrar.DomainInformation -- registrar.User registrar.DomainInformation -- registrar.DomainRequest registrar.DomainInformation -- registrar.Contact @@ -142,43 +242,6 @@ class "registrar.DraftDomain " as registrar.DraftDomain #d6f4e9 { } -class "registrar.Domain " as registrar.Domain #d6f4e9 { - domain - -- - + id (BigAutoField) - + created_at (DateTimeField) - + updated_at (DateTimeField) - + name (CharField) - -- -} - - -class "registrar.HostIP " as registrar.HostIP #d6f4e9 { - host ip - -- - + id (BigAutoField) - + created_at (DateTimeField) - + updated_at (DateTimeField) - + address (CharField) - ~ host (ForeignKey) - -- -} -registrar.HostIP -- registrar.Host - - -class "registrar.Host " as registrar.Host #d6f4e9 { - host - -- - + id (BigAutoField) - + created_at (DateTimeField) - + updated_at (DateTimeField) - + name (CharField) - ~ domain (ForeignKey) - -- -} -registrar.Host -- registrar.Domain - - class "registrar.UserDomainRole " as registrar.UserDomainRole #d6f4e9 { user domain role -- @@ -208,47 +271,49 @@ class "registrar.DomainInvitation " as registrar.DomainInvitation #d6 registrar.DomainInvitation -- registrar.Domain -class "registrar.Nameserver " as registrar.Nameserver #d6f4e9 { - nameserver +class "registrar.TransitionDomain " as registrar.TransitionDomain #d6f4e9 { + transition domain -- + id (BigAutoField) + created_at (DateTimeField) + updated_at (DateTimeField) - + name (CharField) - ~ domain (ForeignKey) - ~ host_ptr (OneToOneField) + + username (CharField) + + domain_name (CharField) + + status (CharField) + + email_sent (BooleanField) + + processed (BooleanField) + + generic_org_type (CharField) + + organization_name (CharField) + + federal_type (CharField) + + federal_agency (CharField) + + epp_creation_date (DateField) + + epp_expiration_date (DateField) + + first_name (CharField) + + middle_name (CharField) + + last_name (CharField) + + title (CharField) + + email (EmailField) + + phone (CharField) + + address_line (CharField) + + city (CharField) + + state_territory (CharField) + + zipcode (CharField) -- } -registrar.Nameserver -- registrar.Domain -registrar.Nameserver -- registrar.Host -class "registrar.PublicContact " as registrar.PublicContact #d6f4e9 { - public contact +class "registrar.VerifiedByStaff " as registrar.VerifiedByStaff #d6f4e9 { + verified by staff -- + id (BigAutoField) + created_at (DateTimeField) + updated_at (DateTimeField) - + contact_type (CharField) - + registry_id (CharField) - ~ domain (ForeignKey) - + name (TextField) - + org (TextField) - + street1 (TextField) - + street2 (TextField) - + street3 (TextField) - + city (TextField) - + sp (TextField) - + pc (TextField) - + cc (TextField) - + email (TextField) - + voice (TextField) - + fax (TextField) - + pw (TextField) + + email (EmailField) + ~ requestor (ForeignKey) + + notes (TextField) -- } - -registrar.PublicContact -- registrar.Domain +registrar.VerifiedByStaff -- registrar.User class "registrar.User " as registrar.User #d6f4e9 { @@ -265,7 +330,11 @@ class "registrar.User " as registrar.User #d6f4e9 { + is_staff (BooleanField) + is_active (BooleanField) + date_joined (DateTimeField) + + status (CharField) + phone (PhoneNumberField) + + middle_name (CharField) + + title (CharField) + + verification_type (CharField) # groups (ManyToManyField) # user_permissions (ManyToManyField) # domains (ManyToManyField) @@ -274,6 +343,17 @@ class "registrar.User " as registrar.User #d6f4e9 { registrar.User *--* registrar.Domain +class "registrar.UserGroup " as registrar.UserGroup #d6f4e9 { + User group + -- + - id (AutoField) + + name (CharField) + ~ group_ptr (OneToOneField) + # permissions (ManyToManyField) + -- +} + + class "registrar.Website " as registrar.Website #d6f4e9 { website -- @@ -285,6 +365,29 @@ class "registrar.Website " as registrar.Website #d6f4e9 { } +class "registrar.WaffleFlag " as registrar.WaffleFlag #d6f4e9 { + waffle flag + -- + + id (BigAutoField) + + name (CharField) + + everyone (BooleanField) + + percent (DecimalField) + + testing (BooleanField) + + superusers (BooleanField) + + staff (BooleanField) + + authenticated (BooleanField) + + languages (TextField) + + rollout (BooleanField) + + note (TextField) + + created (DateTimeField) + + modified (DateTimeField) + # groups (ManyToManyField) + # users (ManyToManyField) + -- +} +registrar.WaffleFlag *--* registrar.User + + @enduml ``` diff --git a/docs/architecture/diagrams/models_diagram.svg b/docs/architecture/diagrams/models_diagram.svg index 49999454c..3fbc7dc10 100644 --- a/docs/architecture/diagrams/models_diagram.svg +++ b/docs/architecture/diagrams/models_diagram.svg @@ -1 +1 @@ -registrarregistrar.ContactRegistrarcontactid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)user (OneToOneField)first_name (TextField)middle_name (TextField)last_name (TextField)title (TextField)email (TextField)phone (PhoneNumberField)registrar.UserRegistraruserid (BigAutoField)password (CharField)last_login (DateTimeField)is_superuser (BooleanField)username (CharField)first_name (CharField)last_name (CharField)email (EmailField)is_staff (BooleanField)is_active (BooleanField)date_joined (DateTimeField)phone (PhoneNumberField)groups (ManyToManyField)user_permissions (ManyToManyField)domains (ManyToManyField)registrar.DomainRequestRegistrardomain requestid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)status (FSMField)creator (ForeignKey)investigator (ForeignKey)organization_type (CharField)federally_recognized_tribe (BooleanField)state_recognized_tribe (BooleanField)tribe_name (TextField)federal_agency (TextField)federal_type (CharField)is_election_board (BooleanField)organization_name (TextField)address_line1 (TextField)address_line2 (CharField)city (TextField)state_territory (CharField)zipcode (CharField)urbanization (TextField)type_of_work (TextField)more_organization_information (TextField)authorizing_official (ForeignKey)approved_domain (OneToOneField)requested_domain (OneToOneField)submitter (ForeignKey)purpose (TextField)no_other_contacts_rationale (TextField)anything_else (TextField)is_policy_acknowledged (BooleanField)current_websites (ManyToManyField)alternative_domains (ManyToManyField)other_contacts (ManyToManyField)registrar.DraftDomainRegistrardraft domainid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (CharField)registrar.DomainRegistrardomainid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (CharField)registrar.WebsiteRegistrarwebsiteid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)website (CharField)registrar.DomainInformationRegistrardomain informationid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)creator (ForeignKey)domain_request (OneToOneField)organization_type (CharField)federally_recognized_tribe (BooleanField)state_recognized_tribe (BooleanField)tribe_name (TextField)federal_agency (TextField)federal_type (CharField)is_election_board (BooleanField)organization_name (TextField)address_line1 (TextField)address_line2 (CharField)city (TextField)state_territory (CharField)zipcode (CharField)urbanization (TextField)type_of_work (TextField)more_organization_information (TextField)authorizing_official (ForeignKey)domain (OneToOneField)submitter (ForeignKey)purpose (TextField)no_other_contacts_rationale (TextField)anything_else (TextField)is_policy_acknowledged (BooleanField)security_email (EmailField)other_contacts (ManyToManyField)registrar.HostIPRegistrarhost ipid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)address (CharField)host (ForeignKey)registrar.HostRegistrarhostid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (CharField)domain (ForeignKey)registrar.UserDomainRoleRegistraruser domain roleid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)user (ForeignKey)domain (ForeignKey)role (TextField)registrar.DomainInvitationRegistrardomain invitationid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)email (EmailField)domain (ForeignKey)status (FSMField)registrar.NameserverRegistrarnameserverid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (CharField)domain (ForeignKey)host_ptr (OneToOneField)registrar.PublicContactRegistrarpublic contactid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)contact_type (CharField)registry_id (CharField)domain (ForeignKey)name (TextField)org (TextField)street1 (TextField)street2 (TextField)street3 (TextField)city (TextField)sp (TextField)pc (TextField)cc (TextField)email (TextField)voice (TextField)fax (TextField)pw (TextField) \ No newline at end of file +registrarregistrar.ContactRegistrarcontactid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)user (OneToOneField)first_name (CharField)middle_name (CharField)last_name (CharField)title (CharField)email (EmailField)phone (PhoneNumberField)registrar.UserRegistraruserid (BigAutoField)password (CharField)last_login (DateTimeField)is_superuser (BooleanField)username (CharField)first_name (CharField)last_name (CharField)email (EmailField)is_staff (BooleanField)is_active (BooleanField)date_joined (DateTimeField)status (CharField)phone (PhoneNumberField)middle_name (CharField)title (CharField)verification_type (CharField)groups (ManyToManyField)user_permissions (ManyToManyField)domains (ManyToManyField)registrar.HostRegistrarhostid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (CharField)domain (ForeignKey)registrar.DomainRegistrardomainid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (DomainField)state (FSMField)expiration_date (DateField)security_contact_registry_id (TextField)deleted (DateField)first_ready (DateField)registrar.HostIPRegistrarhost ipid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)address (CharField)host (ForeignKey)registrar.PublicContactRegistrarpublic contactid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)contact_type (CharField)registry_id (CharField)domain (ForeignKey)name (CharField)org (CharField)street1 (CharField)street2 (CharField)street3 (CharField)city (CharField)sp (CharField)pc (CharField)cc (CharField)email (EmailField)voice (CharField)fax (CharField)pw (CharField)registrar.FederalAgencyRegistrarFederal agencyid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)agency (CharField)registrar.DomainRequestRegistrardomain requestid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)status (FSMField)rejection_reason (TextField)federal_agency (ForeignKey)creator (ForeignKey)investigator (ForeignKey)generic_org_type (CharField)is_election_board (BooleanField)organization_type (CharField)federally_recognized_tribe (BooleanField)state_recognized_tribe (BooleanField)tribe_name (CharField)federal_type (CharField)organization_name (CharField)address_line1 (CharField)address_line2 (CharField)city (CharField)state_territory (CharField)zipcode (CharField)urbanization (CharField)about_your_organization (TextField)authorizing_official (ForeignKey)approved_domain (OneToOneField)requested_domain (OneToOneField)submitter (ForeignKey)purpose (TextField)no_other_contacts_rationale (TextField)anything_else (TextField)has_anything_else_text (BooleanField)cisa_representative_email (EmailField)has_cisa_representative (BooleanField)is_policy_acknowledged (BooleanField)submission_date (DateField)notes (TextField)current_websites (ManyToManyField)alternative_domains (ManyToManyField)other_contacts (ManyToManyField)registrar.DraftDomainRegistrardraft domainid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (CharField)registrar.WebsiteRegistrarwebsiteid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)website (CharField)registrar.DomainInformationRegistrardomain informationid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)federal_agency (ForeignKey)creator (ForeignKey)domain_request (OneToOneField)generic_org_type (CharField)organization_type (CharField)federally_recognized_tribe (BooleanField)state_recognized_tribe (BooleanField)tribe_name (CharField)federal_type (CharField)is_election_board (BooleanField)organization_name (CharField)address_line1 (CharField)address_line2 (CharField)city (CharField)state_territory (CharField)zipcode (CharField)urbanization (CharField)about_your_organization (TextField)authorizing_official (ForeignKey)domain (OneToOneField)submitter (ForeignKey)purpose (TextField)no_other_contacts_rationale (TextField)anything_else (TextField)cisa_representative_email (EmailField)is_policy_acknowledged (BooleanField)notes (TextField)other_contacts (ManyToManyField)registrar.UserDomainRoleRegistraruser domain roleid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)user (ForeignKey)domain (ForeignKey)role (TextField)registrar.DomainInvitationRegistrardomain invitationid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)email (EmailField)domain (ForeignKey)status (FSMField)registrar.TransitionDomainRegistrartransition domainid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)username (CharField)domain_name (CharField)status (CharField)email_sent (BooleanField)processed (BooleanField)generic_org_type (CharField)organization_name (CharField)federal_type (CharField)federal_agency (CharField)epp_creation_date (DateField)epp_expiration_date (DateField)first_name (CharField)middle_name (CharField)last_name (CharField)title (CharField)email (EmailField)phone (CharField)address_line (CharField)city (CharField)state_territory (CharField)zipcode (CharField)registrar.VerifiedByStaffRegistrarverified by staffid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)email (EmailField)requestor (ForeignKey)notes (TextField)registrar.UserGroupRegistrarUser groupid (AutoField)name (CharField)group_ptr (OneToOneField)permissions (ManyToManyField)registrar.WaffleFlagRegistrarwaffle flagid (BigAutoField)name (CharField)everyone (BooleanField)percent (DecimalField)testing (BooleanField)superusers (BooleanField)staff (BooleanField)authenticated (BooleanField)languages (TextField)rollout (BooleanField)note (TextField)created (DateTimeField)modified (DateTimeField)groups (ManyToManyField)users (ManyToManyField) \ No newline at end of file diff --git a/ops/manifests/manifest-hotgov.yaml b/ops/manifests/manifest-hotgov.yaml new file mode 100644 index 000000000..70cc97ee7 --- /dev/null +++ b/ops/manifests/manifest-hotgov.yaml @@ -0,0 +1,32 @@ +--- +applications: +- name: getgov-hotgov + 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-hotgov.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-hotgov.app.cloud.gov + services: + - getgov-credentials + - getgov-hotgov-database diff --git a/ops/manifests/manifest-litterbox.yaml b/ops/manifests/manifest-litterbox.yaml new file mode 100644 index 000000000..ae899ef3a --- /dev/null +++ b/ops/manifests/manifest-litterbox.yaml @@ -0,0 +1,32 @@ +--- +applications: +- name: getgov-litterbox + 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-litterbox.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-litterbox.app.cloud.gov + services: + - getgov-credentials + - getgov-litterbox-database diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 6063f6bfc..44babc0b1 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -15,6 +15,7 @@ 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 registrar.models.user_domain_role import UserDomainRole from waffle.admin import FlagAdmin from waffle.models import Sample, Switch from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website @@ -588,6 +589,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): resource_classes = [UserResource] form = MyUserAdminForm + change_form_template = "django/admin/user_change_form.html" class Meta: """Contains meta information about this class""" @@ -627,7 +629,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): None, {"fields": ("username", "password", "status", "verification_type")}, ), - ("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}), + ("Personal info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}), ( "Permissions", { @@ -706,8 +708,6 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): ordering = ["first_name", "last_name", "email"] search_help_text = "Search by first name, last name, or email." - change_form_template = "django/admin/email_clipboard_change_form.html" - def get_search_results(self, request, queryset, search_term): """ Override for get_search_results. This affects any upstream model using autocomplete_fields, @@ -787,6 +787,23 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): # users who might not belong to groups return self.analyst_readonly_fields + def change_view(self, request, object_id, form_url="", extra_context=None): + """Add user's related domains and requests to context""" + obj = self.get_object(request, object_id) + + domain_requests = DomainRequest.objects.filter(creator=obj).exclude( + Q(status=DomainRequest.DomainRequestStatus.STARTED) | Q(status=DomainRequest.DomainRequestStatus.WITHDRAWN) + ) + sort_by = request.GET.get("sort_by", "requested_domain__name") + domain_requests = domain_requests.order_by(sort_by) + + user_domain_roles = UserDomainRole.objects.filter(user=obj) + domain_ids = user_domain_roles.values_list("domain_id", flat=True) + domains = Domain.objects.filter(id__in=domain_ids).exclude(state=Domain.State.DELETED) + + extra_context = {"domain_requests": domain_requests, "domains": domains} + return super().change_view(request, object_id, form_url, extra_context) + class HostIPInline(admin.StackedInline): """Edit an ip address on the host page.""" diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 63ce9882c..8ed702665 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -130,7 +130,7 @@ html[data-theme="light"] { // Sets darker color on delete page links. // Remove when dark mode successfully applies to Django delete page. .delete-confirmation .content a:not(.button) { - color: #005288; + color: color('primary'); } } @@ -159,7 +159,7 @@ html[data-theme="dark"] { // Sets darker color on delete page links. // Remove when dark mode successfully applies to Django delete page. .delete-confirmation .content a:not(.button) { - color: #005288; + color: color('primary'); } } @@ -186,6 +186,14 @@ div#content > h2 { margin: units(2) 0 units(1) 0; } +.module ul.padding-0 { + padding: 0 !important; +} + +.module ul.margin-0 { + margin: 0 !important; +} + .change-list { .usa-table--striped tbody tr:nth-child(odd) td, .usa-table--striped tbody tr:nth-child(odd) th, @@ -732,7 +740,7 @@ div.dja__model-description{ a, a:link, a:visited { font-size: medium; - color: #005288 !important; + color: color('primary') !important; } &.dja__model-description--no-overflow { @@ -761,3 +769,7 @@ div.dja__model-description{ .usa-summary-box h3 { color: #{$dhs-blue-60}; } + +.module caption, .inline-group h2 { + text-transform: capitalize; +} diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index c4c379307..8438812c4 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -660,6 +660,8 @@ ALLOWED_HOSTS = [ "getgov-staging.app.cloud.gov", "getgov-development.app.cloud.gov", "getgov-ag.app.cloud.gov", + "getgov-litterbox.app.cloud.gov", + "getgov-hotgov.app.cloud.gov", "getgov-cb.app.cloud.gov", "getgov-bob.app.cloud.gov", "getgov-meoward.app.cloud.gov", diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index c31acacfd..2aa9d224b 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -106,6 +106,12 @@ class UserFixture: "last_name": "Orr", "email": "riley+320@truss.works", }, + { + "username": "76612d84-66b0-4ae9-9870-81e98b9858b6", + "first_name": "Anna", + "last_name": "Gingle", + "email": "annagingle@truss.works", + }, ] STAFF = [ @@ -194,6 +200,12 @@ class UserFixture: "last_name": "Orr-Analyst", "email": "riley+321@truss.works", }, + { + "username": "e1e350b1-cfc1-4753-a6cb-3ae6d912f99c", + "first_name": "Anna-Analyst", + "last_name": "Gingle-Analyst", + "email": "annagingle+analyst@truss.works", + }, ] def load_users(cls, users, group_name, are_superusers=False): diff --git a/src/registrar/templates/django/admin/user_change_form.html b/src/registrar/templates/django/admin/user_change_form.html new file mode 100644 index 000000000..005d67aec --- /dev/null +++ b/src/registrar/templates/django/admin/user_change_form.html @@ -0,0 +1,36 @@ +{% extends 'django/admin/email_clipboard_change_form.html' %} +{% load i18n static %} + +{% block after_related_objects %} +
+

Associated requests and domains

+
+
+

Domain requests

+ +
+
+

Domains

+ +
+
+
+{% endblock %} diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index be7065403..f049388df 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -667,7 +667,7 @@ class MockDb(TestCase): is_election_board=False, ) - meoward_user = get_user_model().objects.create( + self.meoward_user = get_user_model().objects.create( username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com" ) @@ -676,7 +676,7 @@ class MockDb(TestCase): ) _, created = UserDomainRole.objects.get_or_create( - user=meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER + user=self.meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER ) _, created = UserDomainRole.objects.get_or_create( @@ -688,19 +688,21 @@ class MockDb(TestCase): ) _, created = UserDomainRole.objects.get_or_create( - user=meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER + user=self.meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER ) _, created = UserDomainRole.objects.get_or_create( - user=meoward_user, domain=self.domain_11, role=UserDomainRole.Roles.MANAGER + user=self.meoward_user, domain=self.domain_11, role=UserDomainRole.Roles.MANAGER ) _, created = UserDomainRole.objects.get_or_create( - user=meoward_user, domain=self.domain_12, role=UserDomainRole.Roles.MANAGER + user=self.meoward_user, domain=self.domain_12, role=UserDomainRole.Roles.MANAGER ) _, created = DomainInvitation.objects.get_or_create( - email=meoward_user.email, domain=self.domain_1, status=DomainInvitation.DomainInvitationStatus.RETRIEVED + email=self.meoward_user.email, + domain=self.domain_1, + status=DomainInvitation.DomainInvitationStatus.RETRIEVED, ) _, created = DomainInvitation.objects.get_or_create( diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 79c545420..d9b04643e 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -47,6 +47,7 @@ from registrar.models import ( from registrar.models.user_domain_role import UserDomainRole from registrar.models.verified_by_staff import VerifiedByStaff from .common import ( + MockDb, MockSESClient, AuditedAdminMockData, completed_domain_request, @@ -3438,16 +3439,19 @@ class TestListHeaderAdmin(TestCase): User.objects.all().delete() -class TestMyUserAdmin(TestCase): +class TestMyUserAdmin(MockDb): def setUp(self): + super().setUp() admin_site = AdminSite() self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site) self.client = Client(HTTP_HOST="localhost:8080") self.superuser = create_superuser() + self.staffuser = create_user() self.test_helper = GenericTestHelper(admin=self.admin) def tearDown(self): super().tearDown() + DomainRequest.objects.all().delete() User.objects.all().delete() @less_console_noise_decorator @@ -3472,7 +3476,7 @@ class TestMyUserAdmin(TestCase): """ Tests for the correct helper text on this page """ - user = create_user() + user = self.staffuser p = "adminpass" self.client.login(username="superuser", password=p) @@ -3493,10 +3497,11 @@ class TestMyUserAdmin(TestCase): ] self.test_helper.assert_response_contains_distinct_values(response, expected_values) + @less_console_noise_decorator def test_list_display_without_username(self): with less_console_noise(): request = self.client.request().wsgi_request - request.user = create_user() + request.user = self.staffuser list_display = self.admin.get_list_display(request) expected_list_display = [ @@ -3522,7 +3527,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 = self.staffuser fieldsets = self.admin.get_fieldsets(request) expected_fieldsets = ( ( @@ -3540,6 +3545,97 @@ class TestMyUserAdmin(TestCase): ) self.assertEqual(fieldsets, expected_fieldsets) + def test_analyst_can_see_related_domains_and_requests_in_user_form(self): + """Tests if an analyst can see the related domains and domain requests for a user in that user's form""" + + # From MockDb, we have self.meoward_user which we'll use as creator + # Create fake domain requests + domain_request_started = completed_domain_request( + status=DomainRequest.DomainRequestStatus.STARTED, user=self.meoward_user, name="started.gov" + ) + domain_request_submitted = completed_domain_request( + status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.meoward_user, name="submitted.gov" + ) + domain_request_in_review = completed_domain_request( + status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=self.meoward_user, name="in-review.gov" + ) + domain_request_withdrawn = completed_domain_request( + status=DomainRequest.DomainRequestStatus.WITHDRAWN, user=self.meoward_user, name="withdrawn.gov" + ) + domain_request_approved = completed_domain_request( + status=DomainRequest.DomainRequestStatus.APPROVED, user=self.meoward_user, name="approved.gov" + ) + domain_request_rejected = completed_domain_request( + status=DomainRequest.DomainRequestStatus.REJECTED, user=self.meoward_user, name="rejected.gov" + ) + domain_request_ineligible = completed_domain_request( + status=DomainRequest.DomainRequestStatus.INELIGIBLE, user=self.meoward_user, name="ineligible.gov" + ) + + # From MockDb, we have sel.meoward_user who's admin on + # self.domain_1 - READY + # self.domain_2 - DNS_NEEDED + # self.domain_11 - READY + # self.domain_12 - READY + # DELETED: + domain_deleted, _ = Domain.objects.get_or_create( + name="domain_deleted.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(2024, 4, 2)) + ) + _, created = UserDomainRole.objects.get_or_create( + user=self.meoward_user, domain=domain_deleted, role=UserDomainRole.Roles.MANAGER + ) + + p = "userpass" + self.client.login(username="staffuser", password=p) + response = self.client.get( + "/admin/registrar/user/{}/change/".format(self.meoward_user.id), + follow=True, + ) + + # Make sure the page loaded and contains the expected domain request names and links to the domain requests + self.assertEqual(response.status_code, 200) + + self.assertContains(response, domain_request_submitted.requested_domain.name) + expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_submitted.pk]) + self.assertContains(response, expected_href) + + self.assertContains(response, domain_request_in_review.requested_domain.name) + expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_in_review.pk]) + self.assertContains(response, expected_href) + + self.assertContains(response, domain_request_approved.requested_domain.name) + expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_approved.pk]) + self.assertContains(response, expected_href) + + self.assertContains(response, domain_request_rejected.requested_domain.name) + expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_rejected.pk]) + self.assertContains(response, expected_href) + + self.assertContains(response, domain_request_ineligible.requested_domain.name) + expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_ineligible.pk]) + self.assertContains(response, expected_href) + + # We filter out those requests + # STARTED + self.assertNotContains(response, domain_request_started.requested_domain.name) + expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_started.pk]) + self.assertNotContains(response, expected_href) + + # WITHDRAWN + self.assertNotContains(response, domain_request_withdrawn.requested_domain.name) + expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_withdrawn.pk]) + self.assertNotContains(response, expected_href) + + # Make sure the page contains the expected domain names and links to the domains + self.assertContains(response, self.domain_1.name) + expected_href = reverse("admin:registrar_domain_change", args=[self.domain_1.pk]) + self.assertContains(response, expected_href) + + # We filter out DELETED + self.assertNotContains(response, domain_deleted.name) + expected_href = reverse("admin:registrar_domain_change", args=[domain_deleted.pk]) + self.assertNotContains(response, expected_href) + class AuditedAdminTest(TestCase): def setUp(self):