diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index 214cf6076..f2b4303d6 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -25,6 +25,8 @@ 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/') outputs: environment: ${{ steps.var.outputs.environment}} runs-on: "ubuntu-latest" diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index f5815012c..283380236 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -16,6 +16,8 @@ on: - stable - staging - development + - litterbox + - hotgov - cb - bob - meoward diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index 06638aa05..b9393415b 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -16,6 +16,8 @@ on: options: - staging - development + - litterbox + - hotgov - cb - bob - meoward 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 79200d910..19d876728 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 @@ -398,6 +399,39 @@ class CustomLogEntryAdmin(LogEntryAdmin): change_form_template = "admin/change_form_no_submit.html" add_form_template = "admin/change_form_no_submit.html" + # Select log entry to change -> Log entries + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Log entries" + return super().changelist_view(request, extra_context=extra_context) + + # #786: Skipping on updating audit log tab titles for now + # def change_view(self, request, object_id, form_url="", extra_context=None): + # if extra_context is None: + # extra_context = {} + + # log_entry = self.get_object(request, object_id) + + # if log_entry: + # # Reset title to empty string + # extra_context["subtitle"] = "" + # extra_context["tabtitle"] = "" + + # object_repr = log_entry.object_repr # Hold name of the object + # changes = log_entry.changes + + # # Check if this is a log entry for an addition and related to the contact model + # # Created [name] -> Created [name] contact | Change log entry + # if ( + # all(new_value != "None" for field, (old_value, new_value) in changes.items()) + # and log_entry.content_type.model == "contact" + # ): + # extra_context["subtitle"] = f"Created {object_repr} contact" + # extra_context["tabtitle"] = "Change log entry" + + # return super().change_view(request, object_id, form_url, extra_context=extra_context) + class AdminSortFields: _name_sort = ["first_name", "last_name", "email"] @@ -570,6 +604,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""" @@ -609,7 +644,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", { @@ -688,8 +723,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, @@ -769,6 +802,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.""" @@ -793,6 +843,14 @@ class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin): search_help_text = "Search by domain or host name." inlines = [HostIPInline] + # Select host to change -> Host + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Host" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class HostIpResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -808,6 +866,14 @@ class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin): resource_classes = [HostIpResource] model = models.HostIP + # Select host ip to change -> Host ip + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Host IP" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class ContactResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -941,6 +1007,14 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin): return super().change_view(request, object_id, form_url, extra_context=extra_context) + # Select contact to change -> Contacts + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Contacts" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class WebsiteResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -1058,6 +1132,21 @@ class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin): else: return response + # User Domain manager [email] is manager on domain [domain name] -> + # Domain manager [email] on [domain name] + def changeform_view(self, request, object_id=None, form_url="", extra_context=None): + if extra_context is None: + extra_context = {} + + if object_id: + obj = self.get_object(request, object_id) + if obj: + email = obj.user.email + domain_name = obj.domain.name + extra_context["subtitle"] = f"Domain manager {email} on {domain_name}" + + return super().changeform_view(request, object_id, form_url, extra_context=extra_context) + class DomainInvitationAdmin(ListHeaderAdmin): """Custom domain invitation admin class.""" @@ -1094,6 +1183,14 @@ class DomainInvitationAdmin(ListHeaderAdmin): change_form_template = "django/admin/email_clipboard_change_form.html" + # Select domain invitations to change -> Domain invitations + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Domain invitations" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class DomainInformationResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -1234,6 +1331,14 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): readonly_fields.extend([field for field in self.analyst_readonly_fields]) return readonly_fields # Read-only fields for analysts + # Select domain information to change -> Domain information + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Domain information" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class DomainRequestResource(FsmModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -1692,11 +1797,17 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): if next_char.isdigit(): should_apply_default_filter = True + # Select domain request to change -> Domain requests + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Domain requests" + if should_apply_default_filter: # modify the GET of the request to set the selected filter modified_get = copy.deepcopy(request.GET) modified_get["status__in"] = "submitted,in review,action needed" request.GET = modified_get + response = super().changelist_view(request, extra_context=extra_context) return response @@ -2262,6 +2373,14 @@ class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): # If no redirection is needed, return the original response return response + # Select draft domain to change -> Draft domains + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Draft domains" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class PublicContactResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -2306,6 +2425,20 @@ class PublicContactAdmin(ListHeaderAdmin, ImportExportModelAdmin): change_form_template = "django/admin/email_clipboard_change_form.html" autocomplete_fields = ["domain"] + def changeform_view(self, request, object_id=None, form_url="", extra_context=None): + if extra_context is None: + extra_context = {} + + if object_id: + obj = self.get_object(request, object_id) + if obj: + name = obj.name + email = obj.email + registry_id = obj.registry_id + extra_context["subtitle"] = f"{name} <{email}> id: {registry_id}" + + return super().changeform_view(request, object_id, form_url, extra_context=extra_context) + class VerifiedByStaffAdmin(ListHeaderAdmin): list_display = ("email", "requestor", "truncated_notes", "created_at") @@ -2358,6 +2491,14 @@ class UserGroupAdmin(AuditedAdmin): def user_group(self, obj): return obj.name + # Select user groups to change -> User groups + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "User groups" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class WaffleFlagAdmin(FlagAdmin): """Custom admin implementation of django-waffle's Flag class""" 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 851f3550c..9a6792dc7 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -659,6 +659,8 @@ ALLOWED_HOSTS = [ "getgov-stable.app.cloud.gov", "getgov-staging.app.cloud.gov", "getgov-development.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/templates/admin/base_site.html b/src/registrar/templates/admin/base_site.html index dd680cec5..db34fd893 100644 --- a/src/registrar/templates/admin/base_site.html +++ b/src/registrar/templates/admin/base_site.html @@ -26,10 +26,21 @@ {% endblock %} -{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} +{% block title %} + {% if subtitle %} + {{ subtitle }} | + {% endif %} + {% if tabtitle %} + {{ tabtitle }} | + {% else %} + {{ title }} | + {% endif %} + {{ site_title|default:_('Django site admin') }} +{% endblock %} + {% block extrastyle %}{{ block.super }} -{% endblock %} +{% endblock %} {% block header %} {% if not IS_PRODUCTION %} 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 %} +