From a554c6a5507f9fec38c689e289a666f9eff8096d Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Mon, 6 Jan 2025 11:35:44 -0600 Subject: [PATCH 001/139] new logging logic to separate out error logs --- src/registrar/config/settings.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 050950c9b..b6b87c6eb 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -486,14 +486,6 @@ class JsonServerFormatter(ServerFormatter): log_entry = {"server_time": record.server_time, "level": record.levelname, "message": formatted_record} return json.dumps(log_entry) - -# default to json formatted logs -server_formatter, console_formatter = "json.server", "json" - -# don't use json format locally, it makes logs hard to read in console -if "localhost" in env_base_url: - server_formatter, console_formatter = "django.server", "verbose" - LOGGING = { "version": 1, # Don't import Django's existing loggers @@ -526,23 +518,35 @@ LOGGING = { "console": { "level": env_log_level, "class": "logging.StreamHandler", - "formatter": console_formatter, + "formatter": "verbose", + "filters": ["below_error"], }, "django.server": { "level": "INFO", "class": "logging.StreamHandler", - "formatter": server_formatter, + "formatter": "django.server", + }, + "json": { + "level": "ERROR", + "class": "logging.StreamHandler", + "formatter": "json", }, # No file logger is configured, # because containerized apps # do not log to the file system. }, + "filters": { + "below_error": { + "()": "django.utils.log.CallbackFilter", + "callback": lambda record: record.levelno < logging.ERROR, + } + }, # define loggers: these are "sinks" into which # messages are sent for processing "loggers": { # Django's generic logger "django": { - "handlers": ["console"], + "handlers": ["console", "json"], "level": "INFO", "propagate": False, }, From 1cc2bd55f815441d1f719b5872bf4e8f12dcbb07 Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Mon, 6 Jan 2025 15:40:03 -0600 Subject: [PATCH 002/139] test logging without json --- src/registrar/config/settings.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index b6b87c6eb..e0d32274d 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -519,13 +519,16 @@ LOGGING = { "level": env_log_level, "class": "logging.StreamHandler", "formatter": "verbose", - "filters": ["below_error"], + # "filters": ["below_error"], }, "django.server": { "level": "INFO", "class": "logging.StreamHandler", "formatter": "django.server", }, + # log all messages at ERROR level or higher using json formatter + # We do this because error logs often comprise many lines, + # and json formatting makes them easier to parse. "json": { "level": "ERROR", "class": "logging.StreamHandler", @@ -535,7 +538,9 @@ LOGGING = { # because containerized apps # do not log to the file system. }, + # filters are used to filter messages based on a callback function "filters": { + # filter for messages below ERROR level "below_error": { "()": "django.utils.log.CallbackFilter", "callback": lambda record: record.levelno < logging.ERROR, @@ -546,7 +551,7 @@ LOGGING = { "loggers": { # Django's generic logger "django": { - "handlers": ["console", "json"], + "handlers": ["console"], "level": "INFO", "propagate": False, }, From a3346d666c938b9b709c78965250ecffbe51a6df Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Tue, 21 Jan 2025 11:47:53 -0600 Subject: [PATCH 003/139] Revert "test logging without json" This reverts commit 1cc2bd55f815441d1f719b5872bf4e8f12dcbb07. --- src/registrar/config/settings.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index e0d32274d..b6b87c6eb 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -519,16 +519,13 @@ LOGGING = { "level": env_log_level, "class": "logging.StreamHandler", "formatter": "verbose", - # "filters": ["below_error"], + "filters": ["below_error"], }, "django.server": { "level": "INFO", "class": "logging.StreamHandler", "formatter": "django.server", }, - # log all messages at ERROR level or higher using json formatter - # We do this because error logs often comprise many lines, - # and json formatting makes them easier to parse. "json": { "level": "ERROR", "class": "logging.StreamHandler", @@ -538,9 +535,7 @@ LOGGING = { # because containerized apps # do not log to the file system. }, - # filters are used to filter messages based on a callback function "filters": { - # filter for messages below ERROR level "below_error": { "()": "django.utils.log.CallbackFilter", "callback": lambda record: record.levelno < logging.ERROR, @@ -551,7 +546,7 @@ LOGGING = { "loggers": { # Django's generic logger "django": { - "handlers": ["console"], + "handlers": ["console", "json"], "level": "INFO", "propagate": False, }, From db97a3f715c5517c11a0b3e0f03fc235b87ef731 Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Tue, 21 Jan 2025 15:57:38 -0600 Subject: [PATCH 004/139] add logic to conditionally set log level --- ops/manifests/manifest-ab.yaml | 2 ++ ops/manifests/manifest-ad.yaml | 2 ++ ops/manifests/manifest-ag.yaml | 2 ++ ops/manifests/manifest-backup.yaml | 2 ++ ops/manifests/manifest-bob.yaml | 2 ++ ops/manifests/manifest-cb.yaml | 2 ++ ops/manifests/manifest-development.yaml | 2 ++ ops/manifests/manifest-dk.yaml | 2 ++ ops/manifests/manifest-el.yaml | 2 ++ ops/manifests/manifest-es.yaml | 2 ++ ops/manifests/manifest-gd.yaml | 2 ++ ops/manifests/manifest-hotgov.yaml | 2 ++ ops/manifests/manifest-ko.yaml | 2 ++ ops/manifests/manifest-ky.yaml | 2 ++ ops/manifests/manifest-litterbox.yaml | 2 ++ ops/manifests/manifest-meoward.yaml | 2 ++ ops/manifests/manifest-ms.yaml | 2 ++ ops/manifests/manifest-nl.yaml | 2 ++ ops/manifests/manifest-rb.yaml | 2 ++ ops/manifests/manifest-rh.yaml | 2 ++ ops/manifests/manifest-rjm.yaml | 2 ++ ops/manifests/manifest-stable.yaml | 2 ++ ops/manifests/manifest-staging.yaml | 2 ++ src/registrar/config/settings.py | 28 ++++++++++++++++++------- 24 files changed, 67 insertions(+), 7 deletions(-) diff --git a/ops/manifests/manifest-ab.yaml b/ops/manifests/manifest-ab.yaml index 3ca800392..f5a3e2c3c 100644 --- a/ops/manifests/manifest-ab.yaml +++ b/ops/manifests/manifest-ab.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-ab.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-ad.yaml b/ops/manifests/manifest-ad.yaml index 73d6f96ff..6975f9f50 100644 --- a/ops/manifests/manifest-ad.yaml +++ b/ops/manifests/manifest-ad.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-ad.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-ag.yaml b/ops/manifests/manifest-ag.yaml index 68d630f3e..192b58edb 100644 --- a/ops/manifests/manifest-ag.yaml +++ b/ops/manifests/manifest-ag.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-ag.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-backup.yaml b/ops/manifests/manifest-backup.yaml index ab9e36d68..194b6e91c 100644 --- a/ops/manifests/manifest-backup.yaml +++ b/ops/manifests/manifest-backup.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-backup.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-bob.yaml b/ops/manifests/manifest-bob.yaml index f39d9e145..7af7e1df5 100644 --- a/ops/manifests/manifest-bob.yaml +++ b/ops/manifests/manifest-bob.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-bob.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-cb.yaml b/ops/manifests/manifest-cb.yaml index b9be98d27..e08f800fa 100644 --- a/ops/manifests/manifest-cb.yaml +++ b/ops/manifests/manifest-cb.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-cb.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-development.yaml b/ops/manifests/manifest-development.yaml index 23558ba4c..957cb0227 100644 --- a/ops/manifests/manifest-development.yaml +++ b/ops/manifests/manifest-development.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-development.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-dk.yaml b/ops/manifests/manifest-dk.yaml index 071efb416..6afbe9321 100644 --- a/ops/manifests/manifest-dk.yaml +++ b/ops/manifests/manifest-dk.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-dk.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-el.yaml b/ops/manifests/manifest-el.yaml index 4c7d4d4e4..ee5673700 100644 --- a/ops/manifests/manifest-el.yaml +++ b/ops/manifests/manifest-el.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-el.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-es.yaml b/ops/manifests/manifest-es.yaml index 7fd19b7a0..f0fc73d7e 100644 --- a/ops/manifests/manifest-es.yaml +++ b/ops/manifests/manifest-es.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-es.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-gd.yaml b/ops/manifests/manifest-gd.yaml index 89a7c2169..5c4f83cc5 100644 --- a/ops/manifests/manifest-gd.yaml +++ b/ops/manifests/manifest-gd.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-gd.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-hotgov.yaml b/ops/manifests/manifest-hotgov.yaml index 70cc97ee7..2aa37817a 100644 --- a/ops/manifests/manifest-hotgov.yaml +++ b/ops/manifests/manifest-hotgov.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-hotgov.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-ko.yaml b/ops/manifests/manifest-ko.yaml index a69493f9b..adc3dcc89 100644 --- a/ops/manifests/manifest-ko.yaml +++ b/ops/manifests/manifest-ko.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-ko.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-ky.yaml b/ops/manifests/manifest-ky.yaml index f416d7385..292b0575c 100644 --- a/ops/manifests/manifest-ky.yaml +++ b/ops/manifests/manifest-ky.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-ky.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-litterbox.yaml b/ops/manifests/manifest-litterbox.yaml index ae899ef3a..e2ab5489c 100644 --- a/ops/manifests/manifest-litterbox.yaml +++ b/ops/manifests/manifest-litterbox.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-litterbox.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-meoward.yaml b/ops/manifests/manifest-meoward.yaml index c47d9529d..ba452684e 100644 --- a/ops/manifests/manifest-meoward.yaml +++ b/ops/manifests/manifest-meoward.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-meoward.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-ms.yaml b/ops/manifests/manifest-ms.yaml index ac46f5d92..0068dfa02 100644 --- a/ops/manifests/manifest-ms.yaml +++ b/ops/manifests/manifest-ms.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-ms.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: DEBUG + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-nl.yaml b/ops/manifests/manifest-nl.yaml index d74174e7d..fbf3b0f5f 100644 --- a/ops/manifests/manifest-nl.yaml +++ b/ops/manifests/manifest-nl.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-nl.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-rb.yaml b/ops/manifests/manifest-rb.yaml index 570b49dde..02b099bdd 100644 --- a/ops/manifests/manifest-rb.yaml +++ b/ops/manifests/manifest-rb.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-rb.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-rh.yaml b/ops/manifests/manifest-rh.yaml index f44894ce8..abce35140 100644 --- a/ops/manifests/manifest-rh.yaml +++ b/ops/manifests/manifest-rh.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-rh.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-rjm.yaml b/ops/manifests/manifest-rjm.yaml index 048b44e95..b51db1b95 100644 --- a/ops/manifests/manifest-rjm.yaml +++ b/ops/manifests/manifest-rjm.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-rjm.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: console # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/ops/manifests/manifest-stable.yaml b/ops/manifests/manifest-stable.yaml index 80c97339f..438a012a9 100644 --- a/ops/manifests/manifest-stable.yaml +++ b/ops/manifests/manifest-stable.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://manage.get.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: json # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Which OIDC provider to use diff --git a/ops/manifests/manifest-staging.yaml b/ops/manifests/manifest-staging.yaml index 38099cf17..7679b7248 100644 --- a/ops/manifests/manifest-staging.yaml +++ b/ops/manifests/manifest-staging.yaml @@ -21,6 +21,8 @@ applications: DJANGO_BASE_URL: https://getgov-staging.app.cloud.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO + # tell django what log format to use: console or json. See settings.py for more details. + DJANGO_LOG_FORMAT: json # default public site location GETGOV_PUBLIC_SITE_URL: https://get.gov # Flag to disable/enable features in prod environments diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index b6b87c6eb..f2ccf5d93 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -60,6 +60,7 @@ env_db_url = env.dj_db_url("DATABASE_URL") env_debug = env.bool("DJANGO_DEBUG", default=False) env_is_production = env.bool("IS_PRODUCTION", default=False) env_log_level = env.str("DJANGO_LOG_LEVEL", "DEBUG") +env_log_format = env.str("DJANGO_LOG_FORMAT", "console") env_base_url: str = env.str("DJANGO_BASE_URL") env_getgov_public_site_url = env.str("GETGOV_PUBLIC_SITE_URL", "") env_oidc_active_provider = env.str("OIDC_ACTIVE_PROVIDER", "identity sandbox") @@ -485,6 +486,24 @@ class JsonServerFormatter(ServerFormatter): log_entry = {"server_time": record.server_time, "level": record.levelname, "message": formatted_record} return json.dumps(log_entry) + +# Define console handler outside LOGGING so we can conditionally enablefilters +console_handler = { + "level": env_log_level, + "class": "logging.StreamHandler", + "formatter": "verbose", +} + +if env_log_format == "json": + # in production we need everything to be logged as json so that log levels are parsed correctly + django_handlers = ["json"] +else: + # for non-production environments, send non-error messages to console handler + # we do this because json clutters logs when debugging + django_handlers = ["console", "json"] + # Only add below_error filter for non-production environments + console_handler["filters"] = ["below_error"] + LOGGING = { "version": 1, @@ -515,12 +534,7 @@ LOGGING = { # define where log messages will be sent; # each logger can have one or more handlers "handlers": { - "console": { - "level": env_log_level, - "class": "logging.StreamHandler", - "formatter": "verbose", - "filters": ["below_error"], - }, + "console": console_handler, "django.server": { "level": "INFO", "class": "logging.StreamHandler", @@ -546,7 +560,7 @@ LOGGING = { "loggers": { # Django's generic logger "django": { - "handlers": ["console", "json"], + "handlers": django_handlers, "level": "INFO", "propagate": False, }, From 072738cf03113b3535894a171271334eb0b6630c Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:04:37 -0800 Subject: [PATCH 005/139] Add alert to user domain role delete confirmation page --- src/registrar/admin.py | 4 ++++ .../admin/user_domain_role_delete_confirmation.html | 13 +++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/registrar/templates/django/admin/user_domain_role_delete_confirmation.html diff --git a/src/registrar/admin.py b/src/registrar/admin.py index e89147b11..f0e435090 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1369,9 +1369,13 @@ class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin): change_form_template = "django/admin/user_domain_role_change_form.html" + # Override for the delete confirmation page on the domain table (bulk delete action) + delete_selected_confirmation_template = "django/admin/user_domain_role_delete_confirmation.html" + # Fixes a bug where non-superusers are redirected to the main page def delete_view(self, request, object_id, extra_context=None): """Custom delete_view implementation that specifies redirect behaviour""" + self.delete_confirmation_template = "django/admin/user_domain_role_delete_confirmation.html" response = super().delete_view(request, object_id, extra_context) if isinstance(response, HttpResponseRedirect) and not request.user.has_perm("registrar.full_access_permission"): diff --git a/src/registrar/templates/django/admin/user_domain_role_delete_confirmation.html b/src/registrar/templates/django/admin/user_domain_role_delete_confirmation.html new file mode 100644 index 000000000..807502a31 --- /dev/null +++ b/src/registrar/templates/django/admin/user_domain_role_delete_confirmation.html @@ -0,0 +1,13 @@ +{% extends 'admin/delete_confirmation.html' %} +{% load i18n static %} + +{% block content %} + + {{ block.super }} +{% endblock %} From ea31249ef80a0bfd7db00e15578720a4a0598a0c Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:06:15 -0800 Subject: [PATCH 006/139] Move alert to content subtitle --- .../django/admin/user_domain_role_delete_confirmation.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/django/admin/user_domain_role_delete_confirmation.html b/src/registrar/templates/django/admin/user_domain_role_delete_confirmation.html index 807502a31..171f4c3ea 100644 --- a/src/registrar/templates/django/admin/user_domain_role_delete_confirmation.html +++ b/src/registrar/templates/django/admin/user_domain_role_delete_confirmation.html @@ -1,7 +1,7 @@ {% extends 'admin/delete_confirmation.html' %} {% load i18n static %} -{% block content %} +{% block content_subtitle %} From 117bfa9b169a07876354323f1b6b3ba1c5b0ee97 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:07:16 -0800 Subject: [PATCH 027/139] Add space to domain manager deleted email template --- .../templates/emails/domain_manager_deleted_notification.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/domain_manager_deleted_notification.txt b/src/registrar/templates/emails/domain_manager_deleted_notification.txt index b16b74dec..af0b92a8c 100644 --- a/src/registrar/templates/emails/domain_manager_deleted_notification.txt +++ b/src/registrar/templates/emails/domain_manager_deleted_notification.txt @@ -1,7 +1,7 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} Hi,{% if domain_manager and domain_manager.first_name %} {{ domain_manager.first_name }}.{% endif %} -A domain manager was removed from {{ domain.name }}. +A domain manager was removed from {{ domain.name }}. REMOVED BY: {{ removed_by.email }} REMOVED ON: {{ date }} MANAGER REMOVED: {{ manager_removed.email }} From b28ddf296ecf6985e3145d8dfc529d12bd12a4e9 Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Sun, 2 Feb 2025 17:54:53 -0600 Subject: [PATCH 028/139] fix tests --- .../commands/create_federal_portfolio.py | 54 +++++++++++++++++- .../tests/test_management_scripts.py | 55 +++++++++++++------ 2 files changed, 88 insertions(+), 21 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 095d6d9f5..7a90deed1 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -30,6 +30,8 @@ class Command(BaseCommand): self.failed_portfolios = set() self.added_managers = set() self.added_invitations = set() + self.failed_managers = set() + self.failed_invitations = set() def add_arguments(self, parser): """Add command line arguments to create federal portfolios. @@ -114,9 +116,15 @@ class Command(BaseCommand): TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) try: # C901 'Command.handle' is too complex (12) - portfolio = self.handle_populate_portfolio(federal_agency, parse_domains, parse_requests, both) - portfolios.append(portfolio) - + # if the portfolio is already created, we don't want to create it again + portfolio = Portfolio.objects.filter(organization_name=federal_agency.agency) + if portfolio.exists(): + portfolio = portfolio.first() + message = f"Portfolio '{federal_agency.agency}' already exists. Skipping create." + TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message) + else: + portfolio = self.handle_populate_portfolio(federal_agency, parse_domains, parse_requests, both) + portfolios.append(portfolio) logger.debug(f"add_managers: {add_managers}") if add_managers: logger.debug("Adding managers to portfolio") @@ -141,6 +149,25 @@ class Command(BaseCommand): display_as_str=True, ) + if add_managers: + TerminalHelper.log_script_run_summary( + self.added_managers, + self.failed_managers, + [], # can't skip managers, can only add or fail + log_header="----- MANAGERS ADDED -----", + debug=False, + display_as_str=True, + ) + + TerminalHelper.log_script_run_summary( + self.added_invitations, + self.failed_invitations, + [], # can't skip invitations, can only add or fail + log_header="----- INVITATIONS ADDED -----", + debug=False, + display_as_str=True, + ) + # POST PROCESSING STEP: Remove the federal agency if it matches the portfolio name. # We only do this for started domain requests. if parse_requests or both: @@ -190,11 +217,13 @@ class Command(BaseCommand): user=user, defaults={"roles": [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]}, ) + self.added_managers.add(user) if created: logger.info(f"Added manager '{user}' to portfolio '{portfolio}'") else: logger.info(f"Manager '{user}' already exists in portfolio '{portfolio}'") except User.DoesNotExist: + self.failed_managers.add(user) logger.debug(f"User '{user}' does not exist") for manager in invited_managers: @@ -229,13 +258,20 @@ class Command(BaseCommand): logger.info(f"Created portfolio permission for '{user}' to portfolio '{portfolio}'") else: logger.info(f"Retrieved existing portfolio permission for '{user}' to portfolio '{portfolio}'") + + self.added_invitations.add(user) except User.DoesNotExist: PortfolioInvitation.objects.get_or_create( portfolio=portfolio, email=email, defaults={"status": PortfolioInvitation.PortfolioInvitationStatus.INVITED}, ) + self.added_invitations.add(email) logger.info(f"Created portfolio invitation for '{email}' to portfolio '{portfolio}'") + except Exception as exc: + self.failed_invitations.add(email) + logger.error(exc, exc_info=True) + logger.error(f"Failed to create portfolio invitation for '{email}' to portfolio '{portfolio}'") def post_process_started_domain_requests(self, agencies, portfolios): """ @@ -250,13 +286,20 @@ class Command(BaseCommand): # 2. Said portfolio (or portfolios) are only the ones specified at the start of the script. # 3. The domain request is in status "started". # Note: Both names are normalized so excess spaces are stripped and the string is lowercased. + message = f"agencies: {agencies}" + TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) + domain_requests_to_update = DomainRequest.objects.filter( federal_agency__in=agencies, federal_agency__agency__isnull=False, status=DomainRequest.DomainRequestStatus.STARTED, organization_name__isnull=False, ) + message = (f"domain_requests_to_update: {domain_requests_to_update}") + TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) portfolio_set = {normalize_string(portfolio.organization_name) for portfolio in portfolios if portfolio} + message = f"portfolio_set: {portfolio_set}" + TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) # Update the request, assuming the given agency name matches the portfolio name updated_requests = [] @@ -265,6 +308,9 @@ class Command(BaseCommand): if agency_name in portfolio_set: req.federal_agency = None updated_requests.append(req) + + message = f"updated_requests: {updated_requests}" + TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) # Execute the update and Log the results if TerminalHelper.prompt_for_execution( @@ -275,6 +321,8 @@ class Command(BaseCommand): ), prompt_title="Do you wish to commit this update to the database?", ): + message = f"prompted for execution" + TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) DomainRequest.objects.bulk_update(updated_requests, ["federal_agency"]) TerminalHelper.colorful_logger(logger.info, TerminalColors.OKBLUE, "Action completed successfully.") diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index b2b217044..515c32782 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -1460,14 +1460,11 @@ class TestCreateFederalPortfolio(TestCase): first_name="first", last_name="last", email="mango@igorville.gov", federal_agency=self.executive_agency_2 ) - self.portfolio = Portfolio.objects.create(organization_name=self.federal_agency.agency, federal_agency=self.federal_agency, creator=self.user) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): self.domain_request = completed_domain_request( status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type=DomainRequest.OrganizationChoices.CITY, federal_agency=self.federal_agency, - portfolio=self.portfolio, user=self.user, ) self.domain_request.approve() @@ -1479,26 +1476,22 @@ class TestCreateFederalPortfolio(TestCase): status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type=DomainRequest.OrganizationChoices.CITY, federal_agency=self.federal_agency, - portfolio=self.portfolio, user=self.user, organization_name="Test Federal Agency", ) self.domain_request_2.approve() self.domain_info_2 = DomainInformation.objects.filter(domain_request=self.domain_request_2).get() - self.domain_2 = Domain.objects.get(name="icecreamforigorville.gov") self.domain_request_3 = completed_domain_request( name="exec_1.gov", status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, federal_agency=self.executive_agency_1, - portfolio=self.portfolio, user=self.user, organization_name="Executive Agency 1", ) self.domain_request_3.approve() self.domain_info_3 = self.domain_request_3.DomainRequest_info - self.domain = Domain.objects.get(name="exec_1.gov") self.domain_request_4 = completed_domain_request( name="exec_2.gov", @@ -1506,12 +1499,10 @@ class TestCreateFederalPortfolio(TestCase): generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, federal_agency=self.executive_agency_2, user=self.user, - portfolio=self.portfolio, organization_name="Executive Agency 2", ) self.domain_request_4.approve() self.domain_info_4 = self.domain_request_4.DomainRequest_info - self.domain_4 = Domain.objects.get(name="exec_2.gov") def tearDown(self): DomainInformation.objects.all().delete() @@ -1529,7 +1520,7 @@ class TestCreateFederalPortfolio(TestCase): ): call_command("create_federal_portfolio", **kwargs) - @less_console_noise_decorator + # @less_console_noise_decorator def test_post_process_started_domain_requests_existing_portfolio(self): """Ensures that federal agency is cleared when agency name matches portfolio name. As the name implies, this implicitly tests the "post_process_started_domain_requests" function. @@ -1869,7 +1860,10 @@ class TestCreateFederalPortfolio(TestCase): UserDomainRole.objects.create(user=manager2, domain=self.domain, role=UserDomainRole.Roles.MANAGER) # Run the management command - self.run_create_federal_portfolio(agency_name=self.portfolio.organization_name, parse_requests=True, add_managers=True) + self.run_create_federal_portfolio(agency_name=self.federal_agency.agency, parse_requests=True, add_managers=True) + + # Check that the portfolio was created + self.portfolio = Portfolio.objects.get(federal_agency=self.federal_agency) # Check that the users have been added as portfolio managers permissions = UserPortfolioPermission.objects.filter(portfolio=self.portfolio, user__in=[manager1, manager2]) @@ -1890,13 +1884,13 @@ class TestCreateFederalPortfolio(TestCase): email="manager1@example.com", status=DomainInvitation.DomainInvitationStatus.INVITED ) - - # Ensure no existing PortfolioInvitation for the invited email - self.assertFalse(PortfolioInvitation.objects.filter(email="manager1@example.com", portfolio=self.portfolio).exists()) - + # Run the management command self.run_create_federal_portfolio(agency_name=self.federal_agency.agency, parse_requests=True, add_managers=True) + # Check that the portfolio was created + self.portfolio = Portfolio.objects.get(federal_agency=self.federal_agency) + # Check that a PortfolioInvitation has been created for the invited email invitation = PortfolioInvitation.objects.get(email="manager1@example.com", portfolio=self.portfolio) @@ -1909,9 +1903,9 @@ class TestCreateFederalPortfolio(TestCase): # Verify that no duplicate invitations are created self.run_create_federal_portfolio(agency_name=self.federal_agency.agency, parse_requests=True, add_managers=True) - duplicated_invitations = PortfolioInvitation.objects.filter(email="manager1@example.com", portfolio=self.portfolio) + invitations = PortfolioInvitation.objects.filter(email="manager1@example.com", portfolio=self.portfolio) self.assertEqual( - duplicated_invitations.count(), + invitations.count(), 1, "Duplicate PortfolioInvitation should not be created for the same email and portfolio." ) @@ -1922,6 +1916,9 @@ class TestCreateFederalPortfolio(TestCase): # Create a manager manager = User.objects.create(username="manager", email="manager@example.com") UserDomainRole.objects.create(user=manager, domain=self.domain, role=UserDomainRole.Roles.MANAGER) + + # Create a pre-existing portfolio + self.portfolio = Portfolio.objects.create(organization_name=self.federal_agency.agency, federal_agency=self.federal_agency, creator=self.user) # Manually add the manager to the portfolio UserPortfolioPermission.objects.create(portfolio=self.portfolio, user=manager, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]) @@ -1931,4 +1928,26 @@ class TestCreateFederalPortfolio(TestCase): # Ensure that the manager is not duplicated permissions = UserPortfolioPermission.objects.filter(portfolio=self.portfolio, user=manager) - self.assertEqual(permissions.count(), 1) \ No newline at end of file + self.assertEqual(permissions.count(), 1) + + @less_console_noise_decorator + def test_add_managers_portfolio_already_exists(self): + """Test that managers are skipped when the portfolio already exists.""" + + # Create a pre-existing portfolio + self.portfolio = Portfolio.objects.create(organization_name=self.federal_agency.agency, federal_agency=self.federal_agency, creator=self.user) + + # Create users and assign them as domain managers + manager1 = User.objects.create(username="manager1", email="manager1@example.com") + manager2 = User.objects.create(username="manager2", email="manager2@example.com") + UserDomainRole.objects.create(user=manager1, domain=self.domain, role=UserDomainRole.Roles.MANAGER) + UserDomainRole.objects.create(user=manager2, domain=self.domain, role=UserDomainRole.Roles.MANAGER) + + # Run the management command + self.run_create_federal_portfolio(agency_name=self.federal_agency.agency, parse_requests=True, add_managers=True) + + # Check that managers were added to the portfolio + permissions = UserPortfolioPermission.objects.filter(portfolio=self.portfolio, user__in=[manager1, manager2]) + self.assertEqual(permissions.count(), 2) + for perm in permissions: + self.assertIn(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, perm.roles) From bdd57c8cfda0abbaa7f3e3a0e8e1b9b8460ceb93 Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Mon, 3 Feb 2025 11:20:09 -0600 Subject: [PATCH 029/139] linter and final test fixes --- .../commands/create_federal_portfolio.py | 42 ++++--- .../tests/test_management_scripts.py | 118 +++++++++++------- 2 files changed, 94 insertions(+), 66 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 2f2cc2625..71004dc49 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -89,7 +89,7 @@ class Command(BaseCommand): help="Only add suborganizations to newly created portfolios, skip existing ones.", ) - def handle(self, **options): + def handle(self, **options): # noqa: C901 agency_name = options.get("agency_name") branch = options.get("branch") parse_requests = options.get("parse_requests") @@ -116,7 +116,6 @@ class Command(BaseCommand): ) else: raise CommandError(f"Cannot find '{branch}' federal agencies in our database.") - portfolios = [] for federal_agency in agencies: message = f"Processing federal agency '{federal_agency.agency}'..." @@ -127,6 +126,8 @@ class Command(BaseCommand): federal_agency, parse_domains, parse_requests, both, skip_existing_portfolios ) portfolios.append(portfolio) + if add_managers: + self.add_managers_to_portfolio(portfolio) except Exception as exec: self.failed_portfolios.add(federal_agency) logger.error(exec) @@ -192,31 +193,32 @@ class Command(BaseCommand): This includes adding them to the correct group and creating portfolio invitations. """ logger.info(f"Adding managers for portfolio {portfolio}") - + # Fetch all domains associated with the portfolio domains = Domain.objects.filter(domain_info__portfolio=portfolio) logger.debug(f"domains: {domains}") - domain_managers = set() + domain_managers: set[int] = set() # Fetch all users with manager roles for the domains - managers = UserDomainRole.objects.filter( - domain__in=domains, - role=UserDomainRole.Roles.MANAGER - ).values_list('user', flat=True) + managers = UserDomainRole.objects.filter(domain__in=domains, role=UserDomainRole.Roles.MANAGER).values_list( + "user", flat=True + ) domain_managers.update(managers) - invited_managers = set() + invited_managers: set[str] = set() # Get the emails of invited managers for domain in domains: - domain_invitations = DomainInvitation.objects.filter(domain=domain, status=DomainInvitation.DomainInvitationStatus.INVITED).values_list('email', flat=True) + domain_invitations = DomainInvitation.objects.filter( + domain=domain, status=DomainInvitation.DomainInvitationStatus.INVITED + ).values_list("email", flat=True) invited_managers.update(domain_invitations) logger.debug(f"invited_managers: {invited_managers}") - for manager in domain_managers: + for id in domain_managers: try: # manager is a user id - user = User.objects.get(id=manager) + user = User.objects.get(id=id) _, created = UserPortfolioPermission.objects.get_or_create( portfolio=portfolio, user=user, @@ -230,9 +232,9 @@ class Command(BaseCommand): except User.DoesNotExist: self.failed_managers.add(user) logger.debug(f"User '{user}' does not exist") - - for manager in invited_managers: - self.create_portfolio_invitation(portfolio, manager) + + for email in invited_managers: + self.create_portfolio_invitation(portfolio, email) def create_portfolio_invitation(self, portfolio: Portfolio, email: str): """ @@ -252,18 +254,18 @@ class Command(BaseCommand): logger.info(f"Created portfolio invitation for '{user}' to portfolio '{portfolio}'") else: logger.info(f"Retrieved existing portfolio invitation for '{user}' to portfolio '{portfolio}'") - + # Assign portfolio permissions _, created = UserPortfolioPermission.objects.get_or_create( portfolio=portfolio, user=user, - defaults={"role": UserPortfolioPermission.RoleChoices.MANAGER}, + defaults={"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER}, ) if created: logger.info(f"Created portfolio permission for '{user}' to portfolio '{portfolio}'") else: logger.info(f"Retrieved existing portfolio permission for '{user}' to portfolio '{portfolio}'") - + self.added_invitations.add(user) except User.DoesNotExist: PortfolioInvitation.objects.get_or_create( @@ -316,7 +318,7 @@ class Command(BaseCommand): if agency_name in portfolio_set: req.federal_agency = None updated_requests.append(req) - + message = f"updated_requests: {updated_requests}" TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) @@ -329,7 +331,7 @@ class Command(BaseCommand): ), prompt_title="Do you wish to commit this update to the database?", ): - message = f"prompted for execution" + message = "prompted for execution" TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) DomainRequest.objects.bulk_update(updated_requests, ["federal_agency"]) TerminalHelper.colorful_logger(logger.info, TerminalColors.OKBLUE, "Action completed successfully.") diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 2ca4e4ffe..ac507cdf2 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -3,17 +3,11 @@ import boto3_mocking # type: ignore from datetime import date, datetime, time from django.core.management import call_command from django.test import TestCase, override_settings -<<<<<<< HEAD -from registrar.models.portfolio_invitation import PortfolioInvitation -from registrar.models.senior_official import SeniorOfficial -from registrar.models.user_portfolio_permission import UserPortfolioPermission -from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices -======= from registrar.models.domain_group import DomainGroup from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.senior_official import SeniorOfficial from registrar.models.user_portfolio_permission import UserPortfolioPermission ->>>>>>> origin/main +from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices from registrar.utility.constants import BranchChoices from django.utils import timezone from django.utils.module_loading import import_string @@ -1472,7 +1466,7 @@ class TestCreateFederalPortfolio(TestCase): self.executive_so_2 = SeniorOfficial.objects.create( first_name="first", last_name="last", email="mango@igorville.gov", federal_agency=self.executive_agency_2 ) - + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): self.domain_request = completed_domain_request( status=DomainRequest.DomainRequestStatus.IN_REVIEW, @@ -1533,7 +1527,7 @@ class TestCreateFederalPortfolio(TestCase): ): call_command("create_federal_portfolio", **kwargs) - # @less_console_noise_decorator + @less_console_noise_decorator def test_post_process_started_domain_requests_existing_portfolio(self): """Ensures that federal agency is cleared when agency name matches portfolio name. As the name implies, this implicitly tests the "post_process_started_domain_requests" function. @@ -1862,7 +1856,6 @@ class TestCreateFederalPortfolio(TestCase): self.assertEqual(existing_portfolio.notes, "Old notes") self.assertEqual(existing_portfolio.creator, self.user) -<<<<<<< HEAD @less_console_noise_decorator def test_add_managers_from_domains(self): """Test that all domain managers are added as portfolio managers.""" @@ -1872,56 +1865,55 @@ class TestCreateFederalPortfolio(TestCase): manager2 = User.objects.create(username="manager2", email="manager2@example.com") UserDomainRole.objects.create(user=manager1, domain=self.domain, role=UserDomainRole.Roles.MANAGER) UserDomainRole.objects.create(user=manager2, domain=self.domain, role=UserDomainRole.Roles.MANAGER) - + # Run the management command - self.run_create_federal_portfolio(agency_name=self.federal_agency.agency, parse_requests=True, add_managers=True) + self.run_create_federal_portfolio(agency_name=self.federal_agency.agency, parse_domains=True, add_managers=True) # Check that the portfolio was created self.portfolio = Portfolio.objects.get(federal_agency=self.federal_agency) - + # Check that the users have been added as portfolio managers permissions = UserPortfolioPermission.objects.filter(portfolio=self.portfolio, user__in=[manager1, manager2]) - print(UserPortfolioPermission.objects.all()) - # Check that the users have been added as portfolio managers + # Check that the users have been added as portfolio managers self.assertEqual(permissions.count(), 2) for perm in permissions: self.assertIn(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, perm.roles) - + @less_console_noise_decorator def test_add_invited_managers(self): """Test that invited domain managers receive portfolio invitations.""" # create a domain invitation for the manager _ = DomainInvitation.objects.create( - domain=self.domain, - email="manager1@example.com", - status=DomainInvitation.DomainInvitationStatus.INVITED - ) - + domain=self.domain, email="manager1@example.com", status=DomainInvitation.DomainInvitationStatus.INVITED + ) + # Run the management command - self.run_create_federal_portfolio(agency_name=self.federal_agency.agency, parse_requests=True, add_managers=True) - + self.run_create_federal_portfolio(agency_name=self.federal_agency.agency, parse_domains=True, add_managers=True) + # Check that the portfolio was created self.portfolio = Portfolio.objects.get(federal_agency=self.federal_agency) # Check that a PortfolioInvitation has been created for the invited email invitation = PortfolioInvitation.objects.get(email="manager1@example.com", portfolio=self.portfolio) - + # Verify the status of the invitation remains INVITED self.assertEqual( invitation.status, PortfolioInvitation.PortfolioInvitationStatus.INVITED, - "PortfolioInvitation status should remain INVITED for non-existent users." + "PortfolioInvitation status should remain INVITED for non-existent users.", ) - + # Verify that no duplicate invitations are created - self.run_create_federal_portfolio(agency_name=self.federal_agency.agency, parse_requests=True, add_managers=True) + self.run_create_federal_portfolio( + agency_name=self.federal_agency.agency, parse_requests=True, add_managers=True + ) invitations = PortfolioInvitation.objects.filter(email="manager1@example.com", portfolio=self.portfolio) self.assertEqual( invitations.count(), 1, - "Duplicate PortfolioInvitation should not be created for the same email and portfolio." + "Duplicate PortfolioInvitation should not be created for the same email and portfolio.", ) @less_console_noise_decorator @@ -1931,41 +1923,76 @@ class TestCreateFederalPortfolio(TestCase): manager = User.objects.create(username="manager", email="manager@example.com") UserDomainRole.objects.create(user=manager, domain=self.domain, role=UserDomainRole.Roles.MANAGER) - # Create a pre-existing portfolio - self.portfolio = Portfolio.objects.create(organization_name=self.federal_agency.agency, federal_agency=self.federal_agency, creator=self.user) - + # Create a pre-existing portfolio + self.portfolio = Portfolio.objects.create( + organization_name=self.federal_agency.agency, federal_agency=self.federal_agency, creator=self.user + ) + # Manually add the manager to the portfolio - UserPortfolioPermission.objects.create(portfolio=self.portfolio, user=manager, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]) - + UserPortfolioPermission.objects.create( + portfolio=self.portfolio, user=manager, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER] + ) + # Run the management command - self.run_create_federal_portfolio(agency_name=self.federal_agency.agency, parse_requests=True, add_managers=True) - + self.run_create_federal_portfolio( + agency_name=self.federal_agency.agency, parse_requests=True, add_managers=True + ) + # Ensure that the manager is not duplicated permissions = UserPortfolioPermission.objects.filter(portfolio=self.portfolio, user=manager) self.assertEqual(permissions.count(), 1) @less_console_noise_decorator - def test_add_managers_portfolio_already_exists(self): + def test_add_managers_skip_existing_portfolios(self): """Test that managers are skipped when the portfolio already exists.""" - - # Create a pre-existing portfolio - self.portfolio = Portfolio.objects.create(organization_name=self.federal_agency.agency, federal_agency=self.federal_agency, creator=self.user) - + + # Create a pre-existing portfolio + self.portfolio = Portfolio.objects.create( + organization_name=self.federal_agency.agency, federal_agency=self.federal_agency, creator=self.user + ) + + domain_request_1 = completed_domain_request( + name="domain1.gov", + status=DomainRequest.DomainRequestStatus.IN_REVIEW, + generic_org_type=DomainRequest.OrganizationChoices.CITY, + federal_agency=self.federal_agency, + user=self.user, + portfolio=self.portfolio, + ) + domain_request_1.approve() + domain1 = Domain.objects.get(name="domain1.gov") + + domain_request_2 = completed_domain_request( + name="domain2.gov", + status=DomainRequest.DomainRequestStatus.IN_REVIEW, + generic_org_type=DomainRequest.OrganizationChoices.CITY, + federal_agency=self.federal_agency, + user=self.user, + portfolio=self.portfolio, + ) + domain_request_2.approve() + domain2 = Domain.objects.get(name="domain2.gov") + # Create users and assign them as domain managers manager1 = User.objects.create(username="manager1", email="manager1@example.com") manager2 = User.objects.create(username="manager2", email="manager2@example.com") - UserDomainRole.objects.create(user=manager1, domain=self.domain, role=UserDomainRole.Roles.MANAGER) - UserDomainRole.objects.create(user=manager2, domain=self.domain, role=UserDomainRole.Roles.MANAGER) + UserDomainRole.objects.create(user=manager1, domain=domain1, role=UserDomainRole.Roles.MANAGER) + UserDomainRole.objects.create(user=manager2, domain=domain2, role=UserDomainRole.Roles.MANAGER) # Run the management command - self.run_create_federal_portfolio(agency_name=self.federal_agency.agency, parse_requests=True, add_managers=True) - + self.run_create_federal_portfolio( + agency_name=self.federal_agency.agency, + parse_requests=True, + add_managers=True, + skip_existing_portfolios=True, + ) + # Check that managers were added to the portfolio permissions = UserPortfolioPermission.objects.filter(portfolio=self.portfolio, user__in=[manager1, manager2]) self.assertEqual(permissions.count(), 2) for perm in permissions: self.assertIn(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, perm.roles) -======= + def test_skip_existing_portfolios(self): """Tests the skip_existing_portfolios to ensure that it doesn't add suborgs, domain requests, and domain info.""" @@ -2466,4 +2493,3 @@ class TestRemovePortfolios(TestCase): # Check that the portfolio was deleted self.assertFalse(Portfolio.objects.filter(organization_name="Test with suborg").exists()) ->>>>>>> origin/main From dca2f635a287fb845ea1becfdf9c263f0d2908e4 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 3 Feb 2025 13:07:36 -0700 Subject: [PATCH 030/139] Shrink max-width of tables in widescreen. Constrict Action column to 80px. Add Action column updates to Members table. --- src/docker-compose.yml | 2 ++ .../src/js/getgov/table-domain-requests.js | 4 +-- .../assets/src/js/getgov/table-domains.js | 2 +- .../assets/src/js/getgov/table-members.js | 30 +++++++------------ .../assets/src/sass/_theme/_base.scss | 6 +++- .../templates/includes/members_table.html | 2 +- 6 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/docker-compose.yml b/src/docker-compose.yml index 5ad6d0ce6..09bf8243e 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -79,6 +79,8 @@ services: - POSTGRES_DB=app - POSTGRES_USER=user - POSTGRES_PASSWORD=feedabee + ports: + - "5432:5432" node: build: diff --git a/src/registrar/assets/src/js/getgov/table-domain-requests.js b/src/registrar/assets/src/js/getgov/table-domain-requests.js index f667a96b5..9a78a4551 100644 --- a/src/registrar/assets/src/js/getgov/table-domain-requests.js +++ b/src/registrar/assets/src/js/getgov/table-domain-requests.js @@ -116,8 +116,8 @@ export class DomainRequestsTable extends BaseTable { ${request.status} - -
+ +
${markupForSuborganizationRow} - + Extra Actions`; - let tableHeaderRow = this.tableWrapper.querySelector('thead tr'); - tableHeaderRow.appendChild(extraActionsHeader); - } return { 'hasAdditionalActions': hasEditPermission, 'UserPortfolioPermissionChoices' : data.UserPortfolioPermissionChoices @@ -121,15 +109,17 @@ export class MembersTable extends BaseTable { ${last_active.display_value} - - - - ${member.action_label} ${member.name} - + +
+ + + ${member.action_label} ${member.name} + + ${customTableOptions.hasAdditionalActions ? kebabHTML : ''} +
- ${customTableOptions.hasAdditionalActions ? ''+kebabHTML+'' : ''} `; tbody.appendChild(row); if (domainsHTML || permissionsHTML) { diff --git a/src/registrar/assets/src/sass/_theme/_base.scss b/src/registrar/assets/src/sass/_theme/_base.scss index be3b89baf..8ab914e6c 100644 --- a/src/registrar/assets/src/sass/_theme/_base.scss +++ b/src/registrar/assets/src/sass/_theme/_base.scss @@ -1,7 +1,7 @@ @use "uswds-core" as *; @use "cisa_colors" as *; -$widescreen-max-width: 1920px; +$widescreen-max-width: 1536px; //1920px; $widescreen-x-padding: 4.5rem; $hot-pink: #FFC3F9; @@ -275,3 +275,7 @@ abbr[title] { .width-quarter { width: 25%; } + +.width--action-column { + max-width: 80px; +} diff --git a/src/registrar/templates/includes/members_table.html b/src/registrar/templates/includes/members_table.html index cc308619a..18f6b3546 100644 --- a/src/registrar/templates/includes/members_table.html +++ b/src/registrar/templates/includes/members_table.html @@ -59,7 +59,7 @@ role="columnheader" id="header-action" > - Action + Action From a9f208116702fcbd917e85a5b98dad3e1d310c2c Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Mon, 3 Feb 2025 14:23:06 -0600 Subject: [PATCH 031/139] linter fixes --- src/registrar/config/settings.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 4a80c528e..78439188e 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -491,9 +491,10 @@ class JsonServerFormatter(ServerFormatter): log_entry = {"server_time": record.server_time, "level": record.levelname, "message": formatted_record} return json.dumps(log_entry) - + + # If we're running locally we don't want json formatting -if 'localhost' in env_base_url: +if "localhost" in env_base_url: django_handlers = ["console"] elif env_log_format == "json": # in production we need everything to be logged as json so that log levels are parsed correctly @@ -533,7 +534,7 @@ LOGGING = { }, # define where log messages will be sent # each logger can have one or more handlers - "handlers": { + "handlers": { "console": { "level": env_log_level, "class": "logging.StreamHandler", From d89f324e6f42f8110e2030ad18bd3b9ebf5dac09 Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Mon, 3 Feb 2025 14:45:47 -0600 Subject: [PATCH 032/139] remove log statements --- .../management/commands/create_federal_portfolio.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 71004dc49..7ed7316be 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -293,8 +293,6 @@ class Command(BaseCommand): # 2. Said portfolio (or portfolios) are only the ones specified at the start of the script. # 3. The domain request is in status "started". # Note: Both names are normalized so excess spaces are stripped and the string is lowercased. - message = f"agencies: {agencies}" - TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) domain_requests_to_update = DomainRequest.objects.filter( federal_agency__in=agencies, @@ -308,8 +306,6 @@ class Command(BaseCommand): return portfolio_set = {normalize_string(portfolio.organization_name) for portfolio in portfolios if portfolio} - message = f"portfolio_set: {portfolio_set}" - TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) # Update the request, assuming the given agency name matches the portfolio name updated_requests = [] @@ -319,9 +315,6 @@ class Command(BaseCommand): req.federal_agency = None updated_requests.append(req) - message = f"updated_requests: {updated_requests}" - TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) - # Execute the update and Log the results if TerminalHelper.prompt_for_execution( system_exit_on_terminate=False, @@ -331,8 +324,6 @@ class Command(BaseCommand): ), prompt_title="Do you wish to commit this update to the database?", ): - message = "prompted for execution" - TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) DomainRequest.objects.bulk_update(updated_requests, ["federal_agency"]) TerminalHelper.colorful_logger(logger.info, TerminalColors.OKBLUE, "Action completed successfully.") From 72da75773efb8ece3d54f3b8beddfc089b89a487 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 3 Feb 2025 13:55:00 -0700 Subject: [PATCH 033/139] Icon alignments --- src/registrar/assets/src/js/getgov/table-domain-requests.js | 2 +- src/registrar/assets/src/js/getgov/table-domains.js | 2 +- src/registrar/assets/src/js/getgov/table-members.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/src/js/getgov/table-domain-requests.js b/src/registrar/assets/src/js/getgov/table-domain-requests.js index 9a78a4551..8556b714f 100644 --- a/src/registrar/assets/src/js/getgov/table-domain-requests.js +++ b/src/registrar/assets/src/js/getgov/table-domain-requests.js @@ -119,7 +119,7 @@ export class DomainRequestsTable extends BaseTable {
- ${request.requested_domain ? request.requested_domain : 'New domain request'} diff --git a/src/registrar/assets/src/js/getgov/table-domains.js b/src/registrar/assets/src/js/getgov/table-domains.js index cb0372584..7eeaebab4 100644 --- a/src/registrar/assets/src/js/getgov/table-domains.js +++ b/src/registrar/assets/src/js/getgov/table-domains.js @@ -58,7 +58,7 @@ export class DomainsTable extends BaseTable { ${markupForSuborganizationRow} - ${domain.name} diff --git a/src/registrar/assets/src/js/getgov/table-members.js b/src/registrar/assets/src/js/getgov/table-members.js index d676e7801..29d140185 100644 --- a/src/registrar/assets/src/js/getgov/table-members.js +++ b/src/registrar/assets/src/js/getgov/table-members.js @@ -110,9 +110,9 @@ export class MembersTable extends BaseTable { ${last_active.display_value} -
+
- ${member.name} From 78f447fad09fa38296ac5391ee5b182b3396f70a Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 3 Feb 2025 14:00:49 -0700 Subject: [PATCH 034/139] cleanup --- src/registrar/assets/src/js/getgov/table-domains.js | 2 +- src/registrar/assets/src/sass/_theme/_base.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/src/js/getgov/table-domains.js b/src/registrar/assets/src/js/getgov/table-domains.js index 7eeaebab4..6536e7f6e 100644 --- a/src/registrar/assets/src/js/getgov/table-domains.js +++ b/src/registrar/assets/src/js/getgov/table-domains.js @@ -56,7 +56,7 @@ export class DomainsTable extends BaseTable { ${markupForSuborganizationRow} - +

Invited domain managers

    - {% for item in value.invitations.all %} + {% for item in value.active_invitations.all %}
  • {{ item.email }}
  • {% endfor %}
From 394fc265d7294ef8eb472d7a5cdad5391820c5d9 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Tue, 4 Feb 2025 09:45:53 -0500 Subject: [PATCH 041/139] updated script --- .github/workflows/delete-and-recreate-db.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/delete-and-recreate-db.yaml b/.github/workflows/delete-and-recreate-db.yaml index 96a698392..e52397d32 100644 --- a/.github/workflows/delete-and-recreate-db.yaml +++ b/.github/workflows/delete-and-recreate-db.yaml @@ -84,8 +84,7 @@ jobs: #if it is taking way to long and the annoying “instance starting” line that keeps repeating, then run following two commands in a separate window. This will interrupt the death loop where it keeps hitting an error with it failing health checks #create the cache table and run migrations cf run-task getgov-$DESTINATION_ENVIRONMENT --command 'python manage.py createcachetable' --name createcachetable - cf run-task getgov-$DESTINATION_ENVIRONMENT --command 'python manage.py migrate' --name migrate + cf run-task getgov-$DESTINATION_ENVIRONMENT --wait --command 'python manage.py migrate' --name migrate - #check that your cloud.gov logs show this is done before you run the following command (or be like me and you have to run the command again because you were impatient. Running this before the migrate finishes will cause an error) #load fixtures - cf run-task getgov-$DESTINATION_ENVIRONMENT --command 'python manage.py load' --name loaddata + cf run-task getgov-$DESTINATION_ENVIRONMENT --wait --command 'python manage.py load' --name loaddata From 62e7b0914425960787885f9205ec036eab01e7c9 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Tue, 4 Feb 2025 09:49:42 -0500 Subject: [PATCH 042/139] fixed typos --- docs/developer/workflows/delete-and-recreate-db.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/developer/workflows/delete-and-recreate-db.md b/docs/developer/workflows/delete-and-recreate-db.md index 667ea6fe4..98a2d445a 100644 --- a/docs/developer/workflows/delete-and-recreate-db.md +++ b/docs/developer/workflows/delete-and-recreate-db.md @@ -1,6 +1,6 @@ ## Delete And Recreate Database -This script destroys recreates a database. This is another troubleshooting tool for issues with the database. It +This script destroys and recreates a database. This is another troubleshooting tool for issues with the database. 1. unbinds the database 2. deletes it @@ -11,3 +11,4 @@ This script destroys recreates a database. This is another troubleshooting tool Addition Info in this slack thread: [Slack thread](https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1725495150772119) +[Script](../../../../manage.get.gov/.github/workflows/delete-and-recreate-db.yaml) From 5bd8ec6761ea011c1b98b63ed8e115fc4cc5245f Mon Sep 17 00:00:00 2001 From: asaki222 Date: Tue, 4 Feb 2025 10:46:23 -0500 Subject: [PATCH 043/139] changed the file path --- docs/developer/workflows/delete-and-recreate-db.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer/workflows/delete-and-recreate-db.md b/docs/developer/workflows/delete-and-recreate-db.md index 98a2d445a..970449aa6 100644 --- a/docs/developer/workflows/delete-and-recreate-db.md +++ b/docs/developer/workflows/delete-and-recreate-db.md @@ -10,5 +10,5 @@ This script destroys and recreates a database. This is another troubleshooting t Addition Info in this slack thread: -[Slack thread](https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1725495150772119) -[Script](../../../../manage.get.gov/.github/workflows/delete-and-recreate-db.yaml) +- [Slack thread](https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1725495150772119) +- [Script](.github/workflows/delete-and-recreate-db.yaml) From 1a3e83b507d0e5f1bcb8779950fb158539d6d61d Mon Sep 17 00:00:00 2001 From: asaki222 Date: Tue, 4 Feb 2025 10:49:12 -0500 Subject: [PATCH 044/139] removed script link --- docs/developer/workflows/delete-and-recreate-db.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/developer/workflows/delete-and-recreate-db.md b/docs/developer/workflows/delete-and-recreate-db.md index 970449aa6..7b378ce47 100644 --- a/docs/developer/workflows/delete-and-recreate-db.md +++ b/docs/developer/workflows/delete-and-recreate-db.md @@ -11,4 +11,3 @@ This script destroys and recreates a database. This is another troubleshooting t Addition Info in this slack thread: - [Slack thread](https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1725495150772119) -- [Script](.github/workflows/delete-and-recreate-db.yaml) From b1f2dddd99a0f2592d4f75536d92888447d44c96 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 4 Feb 2025 12:16:28 -0500 Subject: [PATCH 045/139] fixed portfolio members table, assigned domains --- src/registrar/models/domain.py | 4 ++-- src/registrar/views/portfolio_members_json.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 4154c5575..649b3f93d 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1177,10 +1177,10 @@ class Domain(TimeStampedModel, DomainHelper): elif self.state == self.State.UNKNOWN or self.state == self.State.DNS_NEEDED: return "DNS needed" return self.state.capitalize() - + def active_invitations(self): """Returns only the active invitations (those with status 'invited').""" - return self.invitations.filter(status=DomainInvitation.DomainInvitationStatus.INVITED) + return self.invitations.filter(status=DomainInvitation.DomainInvitationStatus.INVITED) def map_epp_contact_to_public_contact(self, contact: eppInfo.InfoContactResultData, contact_id, contact_type): """Maps the Epp contact representation to a PublicContact object. diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index a45ad66e9..29dc6a71c 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -123,7 +123,11 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): # Subquery to get concatenated domain information for each email domain_invitations = ( - DomainInvitation.objects.filter(email=OuterRef("email"), domain__domain_info__portfolio=portfolio) + DomainInvitation.objects.filter( + email=OuterRef("email"), + domain__domain_info__portfolio=portfolio, + status=DomainInvitation.DomainInvitationStatus.INVITED, + ) .annotate( concatenated_info=Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField()) ) From 3f7d1b0524824033d6535672f225f3239989f5cb Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 4 Feb 2025 12:24:46 -0500 Subject: [PATCH 046/139] unblock invitation when previously retrieved invitation --- src/registrar/models/utility/portfolio_helper.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py index 8c42b80c7..7c82413ae 100644 --- a/src/registrar/models/utility/portfolio_helper.py +++ b/src/registrar/models/utility/portfolio_helper.py @@ -1,5 +1,6 @@ from registrar.utility import StrEnum from django.db import models +from django.db.models import Q from django.apps import apps from django.forms import ValidationError from registrar.utility.waffle import flag_is_active_for_user @@ -136,9 +137,10 @@ def validate_user_portfolio_permission(user_portfolio_permission): "Based on current waffle flag settings, users cannot be assigned to multiple portfolios." ) - existing_invitations = PortfolioInvitation.objects.exclude( - portfolio=user_portfolio_permission.portfolio - ).filter(email=user_portfolio_permission.user.email) + existing_invitations = PortfolioInvitation.objects.filter(email=user_portfolio_permission.user.email).exclude( + Q(portfolio=user_portfolio_permission.portfolio) + | Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED) + ) if existing_invitations.exists(): raise ValidationError( "This user is already assigned to a portfolio invitation. " From 7d8b8fa7556f760a74254149e143ffe94a8c760b Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Tue, 4 Feb 2025 13:14:04 -0600 Subject: [PATCH 047/139] remove extra debug statements --- src/registrar/management/commands/create_federal_portfolio.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 7ed7316be..263e68eaa 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -196,7 +196,6 @@ class Command(BaseCommand): # Fetch all domains associated with the portfolio domains = Domain.objects.filter(domain_info__portfolio=portfolio) - logger.debug(f"domains: {domains}") domain_managers: set[int] = set() # Fetch all users with manager roles for the domains @@ -214,7 +213,6 @@ class Command(BaseCommand): ).values_list("email", flat=True) invited_managers.update(domain_invitations) - logger.debug(f"invited_managers: {invited_managers}") for id in domain_managers: try: # manager is a user id @@ -242,9 +240,7 @@ class Command(BaseCommand): If the user already has a portfolio invitation, retreive their invitation and create a portfolio permission. """ try: - logger.debug(f"Creating portfolio invitation for user '{email}'") user = User.objects.get(email=email) - logger.debug(f"user: {user}") _, created = PortfolioInvitation.objects.get_or_create( portfolio=portfolio, user=user, From 77ddc6700aef46fd224c07cf6bbf260c332f8d81 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 4 Feb 2025 14:34:00 -0800 Subject: [PATCH 048/139] Adding all the status filter and custom filter set up for dropdown --- src/registrar/admin.py | 87 ++++++++++++++++++++--- src/registrar/models/domain_invitation.py | 4 ++ 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 31c75e05e..9d4fe80f3 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1478,9 +1478,31 @@ class BaseInvitationAdmin(ListHeaderAdmin): return response +# class DomainInvitationAdminForm(forms.ModelForm): +# """Custom form for DomainInvitation in admin to only allow cancellations.""" + +# STATUS_CHOICES = [ +# ("", "------"), # no action +# ("canceled", "Canceled"), +# ] + +# status = forms.ChoiceField(choices=STATUS_CHOICES, required=False, label="Status") + +# class Meta: +# model = models.DomainInvitation +# fields = "__all__" + +# def clean_status(self): +# # Clean status - we purposely dont edit anything so we dont mess with the state +# status = self.cleaned_data.get("status") +# return status + + class DomainInvitationAdmin(BaseInvitationAdmin): """Custom domain invitation admin class.""" + # form = DomainInvitationAdminForm + class Meta: model = models.DomainInvitation fields = "__all__" @@ -1505,23 +1527,49 @@ class DomainInvitationAdmin(BaseInvitationAdmin): search_help_text = "Search by email or domain." - # Mark the FSM field 'status' as readonly - # to allow admin users to create Domain Invitations - # without triggering the FSM Transition Not Allowed - # error. + # # Mark the FSM field 'status' as readonly + # # to allow admin users to create Domain Invitations + # # without triggering the FSM Transition Not Allowed + # # error. + # readonly_fields = ["status"] + + # Now it can be edited readonly_fields = ["status"] autocomplete_fields = ["domain"] change_form_template = "django/admin/domain_invitation_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) + # # Custom status filter within DomainInvitationAdmin + # class StatusListFilter(admin.SimpleListFilter): + # # custom filter for status field + + # title = _("status") + # parameter_name = "status" + + # def lookups(self, request, model_admin): + # # only return cancel as option + # return [ + # ('canceled', _('Canceled')), + # ('invited', _('Invited')), + # ('retrieved', _('Retrieved')), + # ] + + # def queryset(self, request, queryset): + # """Filter the queryset based on the selected status.""" + # if self.value(): + # return queryset.filter(status=self.value()) # Apply the filter based on the selected status + # return queryset + + # list_filter = (StatusListFilter,) # Apply the custom filter to the list view + + # # 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) def save_model(self, request, obj, form, change): """ @@ -1531,6 +1579,23 @@ class DomainInvitationAdmin(BaseInvitationAdmin): which will be successful if a single User exists for that email; otherwise, will just continue to create the invitation. """ + + # print("***** IN SAVE_MODEL, OUTSIDE OF CHANGE") + + # # If there is a change and it's related to status, look for canceled + # if change and "status" in form.changed_data: + # print("********* DO WE COME INTO THE CHANGE SECTION") + # if obj.status == DomainInvitation.DomainInvitationStatus.CANCELED: + # # Call the transition method to change the status + # obj.cancel_invitation() + # messages.success(request, f"Invitation for {obj.email} has been canceled.") + # return super().save_model(request, obj, form, change) + + # # if invited/retrieved dont alow manual changes + # if obj.status not in [DomainInvitation.DomainInvitationStatus.INVITED, DomainInvitation.DomainInvitationStatus.RETRIEVED]: + # messages.error(request, "You cannot manually set the status to anything other than 'invited' or 'retrieved'.") + # return + if not change: domain = obj.domain domain_org = getattr(domain.domain_info, "portfolio", None) diff --git a/src/registrar/models/domain_invitation.py b/src/registrar/models/domain_invitation.py index 28089dcb5..3954dea7e 100644 --- a/src/registrar/models/domain_invitation.py +++ b/src/registrar/models/domain_invitation.py @@ -78,6 +78,10 @@ class DomainInvitation(TimeStampedModel): @transition(field="status", source=DomainInvitationStatus.INVITED, target=DomainInvitationStatus.CANCELED) def cancel_invitation(self): """When an invitation is canceled, change the status to canceled""" + # print("***** IN CANCEL_INVITATION SECTION") + # logger.info(f"Invitation for {self.email} to {self.domain} has been canceled.") + # print("WHEN INVITATION IS CANCELED > CHANGE STATUS TO CANCELED") + # Send email here maybe? pass @transition(field="status", source=DomainInvitationStatus.CANCELED, target=DomainInvitationStatus.INVITED) From 8723e35262c9bf0868557f997e185fb4bde0b7be Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 4 Feb 2025 18:35:45 -0500 Subject: [PATCH 049/139] unit test added --- .../models/utility/portfolio_helper.py | 4 +- .../tests/test_views_members_json.py | 16 +++ src/registrar/tests/test_views_portfolio.py | 119 +++++++++++++++--- 3 files changed, 120 insertions(+), 19 deletions(-) diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py index 7c82413ae..0864bded0 100644 --- a/src/registrar/models/utility/portfolio_helper.py +++ b/src/registrar/models/utility/portfolio_helper.py @@ -197,8 +197,8 @@ def validate_portfolio_invitation(portfolio_invitation): if not flag_is_active_for_user(user, "multiple_portfolios"): existing_permissions = UserPortfolioPermission.objects.filter(user=user) - existing_invitations = PortfolioInvitation.objects.exclude(id=portfolio_invitation.id).filter( - email=portfolio_invitation.email + existing_invitations = PortfolioInvitation.objects.filter(email=portfolio_invitation.email).exclude( + Q(id=portfolio_invitation.id) | Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED) ) if existing_permissions.exists(): diff --git a/src/registrar/tests/test_views_members_json.py b/src/registrar/tests/test_views_members_json.py index ceae1e35f..c505421ec 100644 --- a/src/registrar/tests/test_views_members_json.py +++ b/src/registrar/tests/test_views_members_json.py @@ -372,6 +372,21 @@ class GetPortfolioMembersJsonTest(MockEppLib, WebTest): domain=domain3, ) + # create another domain in the portfolio + # but make sure the domain invitation is canceled + domain4 = Domain.objects.create( + name="somedomain4.com", + ) + DomainInformation.objects.create( + creator=self.user, + domain=domain4, + ) + DomainInvitation.objects.create( + email=self.email6, + domain=domain4, + status=DomainInvitation.DomainInvitationStatus.CANCELED, + ) + response = self.app.get(reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id}) self.assertEqual(response.status_code, 200) data = response.json @@ -381,6 +396,7 @@ class GetPortfolioMembersJsonTest(MockEppLib, WebTest): self.assertIn("somedomain1.com", domain_names) self.assertIn("thissecondinvitetestsasubqueryinjson@lets.notbreak", domain_names) self.assertNotIn("somedomain3.com", domain_names) + self.assertNotIn("somedomain4.com", domain_names) @less_console_noise_decorator @override_flag("organization_feature", active=True) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 33f334f7f..a324fc822 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -22,7 +22,7 @@ from registrar.models.utility.portfolio_helper import UserPortfolioPermissionCho from registrar.tests.test_views import TestWithUser from registrar.utility.email import EmailSendingError from registrar.utility.errors import MissingEmailError -from .common import MockSESClient, completed_domain_request, create_test_user, create_user +from .common import MockEppLib, MockSESClient, completed_domain_request, create_test_user, create_user from waffle.testutils import override_flag from django.contrib.sessions.middleware import SessionMiddleware import boto3_mocking # type: ignore @@ -3049,34 +3049,35 @@ class TestRequestingEntity(WebTest): self.assertContains(response, "kepler, AL") -class TestPortfolioInviteNewMemberView(TestWithUser, WebTest): +class TestPortfolioInviteNewMemberView(MockEppLib, WebTest): - @classmethod - def setUpClass(cls): - super().setUpClass() + def setUp(self): + super().setUp() + + self.user = create_test_user() # Create Portfolio - cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Test Portfolio") + self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio") # Add an invited member who has been invited to manage domains - cls.invited_member_email = "invited@example.com" - cls.invitation = PortfolioInvitation.objects.create( - email=cls.invited_member_email, - portfolio=cls.portfolio, + self.invited_member_email = "invited@example.com" + self.invitation = PortfolioInvitation.objects.create( + email=self.invited_member_email, + portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], additional_permissions=[ UserPortfolioPermissionChoices.VIEW_MEMBERS, ], ) - cls.new_member_email = "newmember@example.com" + self.new_member_email = "newmember@example.com" - AllowedEmail.objects.get_or_create(email=cls.new_member_email) + AllowedEmail.objects.get_or_create(email=self.new_member_email) # Assign permissions to the user making requests UserPortfolioPermission.objects.create( - user=cls.user, - portfolio=cls.portfolio, + user=self.user, + portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], additional_permissions=[ UserPortfolioPermissionChoices.VIEW_MEMBERS, @@ -3084,14 +3085,13 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest): ], ) - @classmethod - def tearDownClass(cls): + def tearDown(self): PortfolioInvitation.objects.all().delete() UserPortfolioPermission.objects.all().delete() Portfolio.objects.all().delete() User.objects.all().delete() AllowedEmail.objects.all().delete() - super().tearDownClass() + super().tearDown() @boto3_mocking.patching @less_console_noise_decorator @@ -3136,6 +3136,91 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest): # Check that an email was sent self.assertTrue(mock_client.send_email.called) + @boto3_mocking.patching + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + @patch("registrar.views.portfolios.send_portfolio_invitation_email") + def test_member_invite_for_previously_removed_user(self, mock_send_email): + """Tests the member invitation flow for an existing member which was previously removed.""" + self.client.force_login(self.user) + + # invite, then retrieve an existing user, then remove the user from the portfolio + retrieved_member_email = "retrieved@example.com" + retrieved_user = User.objects.create( + username="retrieved_user", + first_name="Retrieved", + last_name="User", + email=retrieved_member_email, + phone="8003111234", + title="retrieved", + ) + + retrieved_invitation = PortfolioInvitation.objects.create( + email=retrieved_member_email, + portfolio=self.portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], + additional_permissions=[ + UserPortfolioPermissionChoices.VIEW_MEMBERS, + ], + status=PortfolioInvitation.PortfolioInvitationStatus.INVITED, + ) + retrieved_invitation.retrieve() + retrieved_invitation.save() + upp = UserPortfolioPermission.objects.filter( + user=retrieved_user, + portfolio=self.portfolio, + ) + upp.delete() + + # Simulate a session to ensure continuity + session_id = self.client.session.session_key + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + mock_client_class = MagicMock() + mock_client = mock_client_class.return_value + + with boto3_mocking.clients.handler_for("sesv2", mock_client_class): + # Simulate submission of member invite for previously retrieved/removed member + final_response = self.client.post( + reverse("new-member"), + { + "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, + "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, + "member_permissions": "no_access", + "email": retrieved_member_email, + }, + ) + + # Ensure the final submission is successful + self.assertEqual(final_response.status_code, 302) # Redirects + + # Validate Database Changes + # Validate that portfolio invitation was created and retrieved + self.assertFalse( + PortfolioInvitation.objects.filter( + email=retrieved_member_email, + portfolio=self.portfolio, + status=PortfolioInvitation.PortfolioInvitationStatus.INVITED, + ).exists() + ) + # at least one retrieved invitation + self.assertTrue( + PortfolioInvitation.objects.filter( + email=retrieved_member_email, + portfolio=self.portfolio, + status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED, + ).exists() + ) + # Ensure exactly one UserPortfolioPermission exists for the retrieved user + self.assertEqual( + UserPortfolioPermission.objects.filter(user=retrieved_user, portfolio=self.portfolio).count(), + 1, + "Expected exactly one UserPortfolioPermission for the retrieved user." + ) + + @boto3_mocking.patching @less_console_noise_decorator @override_flag("organization_feature", active=True) From 4b523a75ceab37fe36c5e117871ab722c1cef749 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 4 Feb 2025 16:13:07 -0800 Subject: [PATCH 050/139] This is if we had wanted the cancel invitation by the status --- src/registrar/admin.py | 50 +------------------ .../admin/status_with_clipboard.html | 22 ++++++++ .../admin/domain_invitation_change_form.html | 2 +- .../includes/email_clipboard_fieldset.html | 3 ++ 4 files changed, 27 insertions(+), 50 deletions(-) create mode 100644 src/registrar/templates/admin/status_with_clipboard.html diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 9d4fe80f3..feb3b1883 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1533,44 +1533,12 @@ class DomainInvitationAdmin(BaseInvitationAdmin): # # error. # readonly_fields = ["status"] - # Now it can be edited - readonly_fields = ["status"] + readonly_fields = [] autocomplete_fields = ["domain"] change_form_template = "django/admin/domain_invitation_change_form.html" - # # Custom status filter within DomainInvitationAdmin - # class StatusListFilter(admin.SimpleListFilter): - # # custom filter for status field - - # title = _("status") - # parameter_name = "status" - - # def lookups(self, request, model_admin): - # # only return cancel as option - # return [ - # ('canceled', _('Canceled')), - # ('invited', _('Invited')), - # ('retrieved', _('Retrieved')), - # ] - - # def queryset(self, request, queryset): - # """Filter the queryset based on the selected status.""" - # if self.value(): - # return queryset.filter(status=self.value()) # Apply the filter based on the selected status - # return queryset - - # list_filter = (StatusListFilter,) # Apply the custom filter to the list view - - # # 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) - def save_model(self, request, obj, form, change): """ Override the save_model method. @@ -1580,22 +1548,6 @@ class DomainInvitationAdmin(BaseInvitationAdmin): just continue to create the invitation. """ - # print("***** IN SAVE_MODEL, OUTSIDE OF CHANGE") - - # # If there is a change and it's related to status, look for canceled - # if change and "status" in form.changed_data: - # print("********* DO WE COME INTO THE CHANGE SECTION") - # if obj.status == DomainInvitation.DomainInvitationStatus.CANCELED: - # # Call the transition method to change the status - # obj.cancel_invitation() - # messages.success(request, f"Invitation for {obj.email} has been canceled.") - # return super().save_model(request, obj, form, change) - - # # if invited/retrieved dont alow manual changes - # if obj.status not in [DomainInvitation.DomainInvitationStatus.INVITED, DomainInvitation.DomainInvitationStatus.RETRIEVED]: - # messages.error(request, "You cannot manually set the status to anything other than 'invited' or 'retrieved'.") - # return - if not change: domain = obj.domain domain_org = getattr(domain.domain_info, "portfolio", None) diff --git a/src/registrar/templates/admin/status_with_clipboard.html b/src/registrar/templates/admin/status_with_clipboard.html new file mode 100644 index 000000000..a62ca5055 --- /dev/null +++ b/src/registrar/templates/admin/status_with_clipboard.html @@ -0,0 +1,22 @@ +{% load static %} + +
+ {{ field.value | capfirst }} + + + +
+ diff --git a/src/registrar/templates/django/admin/domain_invitation_change_form.html b/src/registrar/templates/django/admin/domain_invitation_change_form.html index 6ce6ed0d1..699760fa8 100644 --- a/src/registrar/templates/django/admin/domain_invitation_change_form.html +++ b/src/registrar/templates/django/admin/domain_invitation_change_form.html @@ -11,4 +11,4 @@
{{ block.super }} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html b/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html index f959f8edf..4c0e63d66 100644 --- a/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html +++ b/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html @@ -7,7 +7,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% block field_other %} {% if field.field.name == "email" %} {% include "admin/input_with_clipboard.html" with field=field.field %} + {% elif field.field.name == "status" %} + {% include "admin/status_with_clipboard.html" with field=field.field %} {% else %} {{ block.super }} {% endif %} {% endblock field_other %} + From 5f545daed876bbbba1dfbe5f065c6c7e8857ab3d Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 4 Feb 2025 19:42:23 -0500 Subject: [PATCH 051/139] lint --- src/registrar/tests/test_views_portfolio.py | 78 ++++++++++----------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index a274e777b..8d06e35da 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -3148,7 +3148,6 @@ class TestPortfolioInviteNewMemberView(MockEppLib, WebTest): # Check that an email was sent self.assertTrue(mock_client.send_email.called) - @boto3_mocking.patching @less_console_noise_decorator @override_flag("organization_feature", active=True) @override_flag("organization_members", active=True) @@ -3189,49 +3188,44 @@ class TestPortfolioInviteNewMemberView(MockEppLib, WebTest): session_id = self.client.session.session_key self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - mock_client_class = MagicMock() - mock_client = mock_client_class.return_value + # Simulate submission of member invite for previously retrieved/removed member + final_response = self.client.post( + reverse("new-member"), + { + "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, + "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, + "member_permissions": "no_access", + "email": retrieved_member_email, + }, + ) - with boto3_mocking.clients.handler_for("sesv2", mock_client_class): - # Simulate submission of member invite for previously retrieved/removed member - final_response = self.client.post( - reverse("new-member"), - { - "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, - "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, - "domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, - "member_permissions": "no_access", - "email": retrieved_member_email, - }, - ) - - # Ensure the final submission is successful - self.assertEqual(final_response.status_code, 302) # Redirects - - # Validate Database Changes - # Validate that portfolio invitation was created and retrieved - self.assertFalse( - PortfolioInvitation.objects.filter( - email=retrieved_member_email, - portfolio=self.portfolio, - status=PortfolioInvitation.PortfolioInvitationStatus.INVITED, - ).exists() - ) - # at least one retrieved invitation - self.assertTrue( - PortfolioInvitation.objects.filter( - email=retrieved_member_email, - portfolio=self.portfolio, - status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED, - ).exists() - ) - # Ensure exactly one UserPortfolioPermission exists for the retrieved user - self.assertEqual( - UserPortfolioPermission.objects.filter(user=retrieved_user, portfolio=self.portfolio).count(), - 1, - "Expected exactly one UserPortfolioPermission for the retrieved user." - ) + # Ensure the final submission is successful + self.assertEqual(final_response.status_code, 302) # Redirects + # Validate Database Changes + # Validate that portfolio invitation was created and retrieved + self.assertFalse( + PortfolioInvitation.objects.filter( + email=retrieved_member_email, + portfolio=self.portfolio, + status=PortfolioInvitation.PortfolioInvitationStatus.INVITED, + ).exists() + ) + # at least one retrieved invitation + self.assertTrue( + PortfolioInvitation.objects.filter( + email=retrieved_member_email, + portfolio=self.portfolio, + status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED, + ).exists() + ) + # Ensure exactly one UserPortfolioPermission exists for the retrieved user + self.assertEqual( + UserPortfolioPermission.objects.filter(user=retrieved_user, portfolio=self.portfolio).count(), + 1, + "Expected exactly one UserPortfolioPermission for the retrieved user.", + ) @boto3_mocking.patching @less_console_noise_decorator From 0e6bc6f07f13122dda318a86ca5e85ea59d71ee3 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 5 Feb 2025 06:29:10 -0500 Subject: [PATCH 052/139] added helpers for role and permissions displays in templates --- src/registrar/models/portfolio_invitation.py | 58 ++++++++++++ .../models/user_portfolio_permission.py | 58 ++++++++++++ .../models/utility/portfolio_helper.py | 89 +++++++++++++++++++ .../templates/emails/portfolio_update.txt | 35 ++++++++ .../emails/portfolio_update_subject.txt | 1 + .../includes/member_permissions_summary.html | 30 +------ 6 files changed, 245 insertions(+), 26 deletions(-) create mode 100644 src/registrar/templates/emails/portfolio_update.txt create mode 100644 src/registrar/templates/emails/portfolio_update_subject.txt diff --git a/src/registrar/models/portfolio_invitation.py b/src/registrar/models/portfolio_invitation.py index 8feeb0794..dd80f946e 100644 --- a/src/registrar/models/portfolio_invitation.py +++ b/src/registrar/models/portfolio_invitation.py @@ -9,6 +9,10 @@ from .utility.portfolio_helper import ( UserPortfolioPermissionChoices, UserPortfolioRoleChoices, cleanup_after_portfolio_member_deletion, + get_domain_requests_display, + get_domains_display, + get_members_display, + get_role_display, validate_portfolio_invitation, ) # type: ignore from .utility.time_stamped_model import TimeStampedModel @@ -85,6 +89,60 @@ class PortfolioInvitation(TimeStampedModel): """ return UserPortfolioPermission.get_portfolio_permissions(self.roles, self.additional_permissions) + @property + def role_display(self): + """ + Returns a human-readable display name for the user's role. + + Uses the `get_role_display` function to determine if the user is an "Admin", + "Basic" member, or has no role assigned. + + Returns: + str: The display name of the user's role. + """ + return get_role_display(self.roles) + + @property + def domains_display(self): + """ + Returns a string representation of the user's domain access level. + + Uses the `get_domains_display` function to determine whether the user has + "Viewer, all" access (can view all domains) or "Viewer, limited" access. + + Returns: + str: The display name of the user's domain permissions. + """ + return get_domains_display(self.roles, self.additional_permissions) + + @property + def domain_requests_display(self): + """ + Returns a string representation of the user's access to domain requests. + + Uses the `get_domain_requests_display` function to determine if the user + is a "Creator" (can create and edit requests), a "Viewer" (can only view requests), + or has "No access" to domain requests. + + Returns: + str: The display name of the user's domain request permissions. + """ + return get_domain_requests_display(self.roles, self.additional_permissions) + + @property + def members_display(self): + """ + Returns a string representation of the user's access to managing members. + + Uses the `get_members_display` function to determine if the user is a + "Manager" (can edit members), a "Viewer" (can view members), or has "No access" + to member management. + + Returns: + str: The display name of the user's member management permissions. + """ + return get_members_display(self.roles, self.additional_permissions) + @transition(field="status", source=PortfolioInvitationStatus.INVITED, target=PortfolioInvitationStatus.RETRIEVED) def retrieve(self): """When an invitation is retrieved, create the corresponding permission. diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 11d9c56e3..372715db2 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -6,6 +6,10 @@ from registrar.models.utility.portfolio_helper import ( DomainRequestPermissionDisplay, MemberPermissionDisplay, cleanup_after_portfolio_member_deletion, + get_domain_requests_display, + get_domains_display, + get_members_display, + get_role_display, validate_user_portfolio_permission, ) from .utility.time_stamped_model import TimeStampedModel @@ -185,6 +189,60 @@ class UserPortfolioPermission(TimeStampedModel): # This is the same as portfolio_permissions & common_forbidden_perms. return portfolio_permissions.intersection(common_forbidden_perms) + @property + def role_display(self): + """ + Returns a human-readable display name for the user's role. + + Uses the `get_role_display` function to determine if the user is an "Admin", + "Basic" member, or has no role assigned. + + Returns: + str: The display name of the user's role. + """ + return get_role_display(self.roles) + + @property + def domains_display(self): + """ + Returns a string representation of the user's domain access level. + + Uses the `get_domains_display` function to determine whether the user has + "Viewer, all" access (can view all domains) or "Viewer, limited" access. + + Returns: + str: The display name of the user's domain permissions. + """ + return get_domains_display(self.roles, self.additional_permissions) + + @property + def domain_requests_display(self): + """ + Returns a string representation of the user's access to domain requests. + + Uses the `get_domain_requests_display` function to determine if the user + is a "Creator" (can create and edit requests), a "Viewer" (can only view requests), + or has "No access" to domain requests. + + Returns: + str: The display name of the user's domain request permissions. + """ + return get_domain_requests_display(self.roles, self.additional_permissions) + + @property + def members_display(self): + """ + Returns a string representation of the user's access to managing members. + + Uses the `get_members_display` function to determine if the user is a + "Manager" (can edit members), a "Viewer" (can view members), or has "No access" + to member management. + + Returns: + str: The display name of the user's member management permissions. + """ + return get_members_display(self.roles, self.additional_permissions) + def clean(self): """Extends clean method to perform additional validation, which can raise errors in django admin.""" super().clean() diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py index 0864bded0..f1f9ef4f2 100644 --- a/src/registrar/models/utility/portfolio_helper.py +++ b/src/registrar/models/utility/portfolio_helper.py @@ -82,6 +82,95 @@ class MemberPermissionDisplay(StrEnum): VIEWER = "Viewer" NONE = "None" +def get_role_display(roles): + """ + Returns a user-friendly display name for a given list of user roles. + + - If the user has the ORGANIZATION_ADMIN role, return "Admin". + - If the user has the ORGANIZATION_MEMBER role, return "Basic". + - If the user has neither role, return "-". + + Args: + roles (list): A list of role strings assigned to the user. + + Returns: + str: The display name for the highest applicable role. + """ + if UserPortfolioRoleChoices.ORGANIZATION_ADMIN in roles: + return "Admin" + elif UserPortfolioRoleChoices.ORGANIZATION_MEMBER in roles: + return "Basic" + else: + return "-" + +def get_domains_display(roles, permissions): + """ + Determines the display name for a user's domain viewing permissions. + + - If the user has the VIEW_ALL_DOMAINS permission, return "Viewer, all". + - Otherwise, return "Viewer, limited". + + Args: + roles (list): A list of role strings assigned to the user. + permissions (list): A list of additional permissions assigned to the user. + + Returns: + str: A string representing the user's domain viewing access. + """ + UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission") + all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions) + if UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS in all_permissions: + return "Viewer, all" + else: + return "Viewer, limited" + +def get_domain_requests_display(roles, permissions): + """ + Determines the display name for a user's domain request permissions. + + - If the user has the EDIT_REQUESTS permission, return "Creator". + - If the user has the VIEW_ALL_REQUESTS permission, return "Viewer". + - Otherwise, return "No access". + + Args: + roles (list): A list of role strings assigned to the user. + permissions (list): A list of additional permissions assigned to the user. + + Returns: + str: A string representing the user's domain request access level. + """ + UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission") + all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions) + if UserPortfolioPermissionChoices.EDIT_REQUESTS in all_permissions: + return "Creator" + elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions: + return "Viewer" + else: + return "No access" + +def get_members_display(roles, permissions): + """ + Determines the display name for a user's member management permissions. + + - If the user has the EDIT_MEMBERS permission, return "Manager". + - If the user has the VIEW_MEMBERS permission, return "Viewer". + - Otherwise, return "No access". + + Args: + roles (list): A list of role strings assigned to the user. + permissions (list): A list of additional permissions assigned to the user. + + Returns: + str: A string representing the user's member management access level. + """ + UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission") + all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions) + if UserPortfolioPermissionChoices.EDIT_MEMBERS in all_permissions: + return "Manager" + elif UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions: + return "Viewer" + else: + return "No access" def validate_user_portfolio_permission(user_portfolio_permission): """ diff --git a/src/registrar/templates/emails/portfolio_update.txt b/src/registrar/templates/emails/portfolio_update.txt new file mode 100644 index 000000000..aa13a9fb9 --- /dev/null +++ b/src/registrar/templates/emails/portfolio_update.txt @@ -0,0 +1,35 @@ +{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} +Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first_name }}.{% endif %} + +Your permissions were updated in the .gov registrar. + +ORGANIZATION: {{ portfolio.organization_name }} +UPDATED BY: {{ requestor_email }} +UPDATED ON: {{ date }} +YOUR PERMISSIONS: {{ permissions.role_display }} + Domains - {{ permissions.domains_display }} + Domain requests - {{ permissions.domain_requests_display }} + Members - {{ permissions.members_display }} + +Your updated permissions are now active in the .gov registrar . + +---------------------------------------------------------------- + +SOMETHING WRONG? +If you have questions or concerns, reach out to the person who updated your +permissions, or reply to this email. + + +THANK YOU +.Gov helps the public identify official, trusted information. Thank you for using a .gov +domain. + +---------------------------------------------------------------- + +The .gov team +Contact us: +Learn about .gov + +The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency +(CISA) +{% endautoescape %} diff --git a/src/registrar/templates/emails/portfolio_update_subject.txt b/src/registrar/templates/emails/portfolio_update_subject.txt new file mode 100644 index 000000000..2cd806a73 --- /dev/null +++ b/src/registrar/templates/emails/portfolio_update_subject.txt @@ -0,0 +1 @@ +Your permissions were updated in the .gov registrar \ No newline at end of file diff --git a/src/registrar/templates/includes/member_permissions_summary.html b/src/registrar/templates/includes/member_permissions_summary.html index 3a91d16f6..95eca0a7e 100644 --- a/src/registrar/templates/includes/member_permissions_summary.html +++ b/src/registrar/templates/includes/member_permissions_summary.html @@ -1,33 +1,11 @@

Member access

-{% if permissions.roles and 'organization_admin' in permissions.roles %} -

Admin

-{% elif permissions.roles and 'organization_member' in permissions.roles %} -

Basic

-{% else %} -

-{% endif %} +

{{ permissions.role_display }}

Domains

-{% if member_has_view_all_domains_portfolio_permission %} -

Viewer, all

-{% else %} -

Viewer, limited

-{% endif %} +

{{ permissions.domains_display }}

Domain requests

-{% if member_has_edit_request_portfolio_permission %} -

Creator

-{% elif member_has_view_all_requests_portfolio_permission %} -

Viewer

-{% else %} -

No access

-{% endif %} +

{{ permissions.domain_requests_display }}

Members

-{% if member_has_edit_members_portfolio_permission %} -

Manager

-{% elif member_has_view_members_portfolio_permission %} -

Viewer

-{% else %} -

No access

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

{{ permissions.members_display }}

From dc3351e8c0daec1b1a0fc8e7e629de44ad4ba3d4 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 5 Feb 2025 16:05:49 -0500 Subject: [PATCH 053/139] revise UI of search bars with labels, create component --- .../includes/member_domains_edit_table.html | 46 ++-------------- .../includes/member_domains_table.html | 52 +++++-------------- src/registrar/templates/includes/search.html | 34 ++++++++++++ 3 files changed, 50 insertions(+), 82 deletions(-) create mode 100644 src/registrar/templates/includes/search.html diff --git a/src/registrar/templates/includes/member_domains_edit_table.html b/src/registrar/templates/includes/member_domains_edit_table.html index 0b8ff005a..cefc30c15 100644 --- a/src/registrar/templates/includes/member_domains_edit_table.html +++ b/src/registrar/templates/includes/member_domains_edit_table.html @@ -36,47 +36,9 @@
- + {% with label_text="Search all domains" item_name="edit-member-domains" aria_label_text="Member domains search component" %} + {% include "includes/search.html" %} + {% endwith %}
@@ -85,7 +47,7 @@ member domains - Assigned domains + Assigned domains Domains diff --git a/src/registrar/templates/includes/member_domains_table.html b/src/registrar/templates/includes/member_domains_table.html index d7839e485..4e63fdbc3 100644 --- a/src/registrar/templates/includes/member_domains_table.html +++ b/src/registrar/templates/includes/member_domains_table.html @@ -1,5 +1,3 @@ -{% load static %} - {% if member %} -
diff --git a/src/registrar/templates/admin/analytics_graph_table.html b/src/registrar/templates/admin/analytics_graph_table.html index c339c63c2..58132d023 100644 --- a/src/registrar/templates/admin/analytics_graph_table.html +++ b/src/registrar/templates/admin/analytics_graph_table.html @@ -1,62 +1,26 @@ -{{ data }} - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {% comment %} + This ugly notation is equivalent to data.property_name.start_date_count.index. + Or represented in the pure python way: data[property_name]["start_date_count"][index] + {% endcomment %} + {% with start_counts=data|get_item:property_name|get_item:"start_date_count" end_counts=data|get_item:property_name|get_item:"end_date_count" %} + {% for org_count_type in data.org_count_types %} + {% with index=forloop.counter %} + + + + + + {% endwith %} + {% endfor %} + {% endwith %}
TypeFor start datefor end dateStart date {{ data.start_date }}End date {{ data.end_date }}
Totaldatadata
Federaldatadata
Interstatedatadata
State/Territorydatadata
Tribaldatadata
Countydatadata
Citydatadata
Special Districtdatadata
School Districtdatadata
Election Boarddatadata
{{ org_count_type }}{{ start_counts|slice:index|last }}{{ end_counts|slice:index|last }}
\ No newline at end of file diff --git a/src/registrar/views/report_views.py b/src/registrar/views/report_views.py index 1b9198c79..596e69a5c 100644 --- a/src/registrar/views/report_views.py +++ b/src/registrar/views/report_views.py @@ -126,28 +126,54 @@ class AnalyticsView(View): # include it in the larger context dictionary so it's available in the template rendering context. # This ensures that the admin interface styling and behavior are consistent with other admin pages. **admin.site.each_context(request), - data=dict( - user_count=models.User.objects.all().count(), - domain_count=models.Domain.objects.all().count(), - ready_domain_count=models.Domain.objects.filter(state=models.Domain.State.READY).count(), - last_30_days_applications=last_30_days_applications.count(), - last_30_days_approved_applications=last_30_days_approved_applications.count(), - average_application_approval_time_last_30_days=avg_approval_time_display, - managed_domains_sliced_at_start_date=managed_domains_sliced_at_start_date, - unmanaged_domains_sliced_at_start_date=unmanaged_domains_sliced_at_start_date, - managed_domains_sliced_at_end_date=managed_domains_sliced_at_end_date, - unmanaged_domains_sliced_at_end_date=unmanaged_domains_sliced_at_end_date, - ready_domains_sliced_at_start_date=ready_domains_sliced_at_start_date, - deleted_domains_sliced_at_start_date=deleted_domains_sliced_at_start_date, - ready_domains_sliced_at_end_date=ready_domains_sliced_at_end_date, - deleted_domains_sliced_at_end_date=deleted_domains_sliced_at_end_date, - requests_sliced_at_start_date=requests_sliced_at_start_date, - submitted_requests_sliced_at_start_date=submitted_requests_sliced_at_start_date, - requests_sliced_at_end_date=requests_sliced_at_end_date, - submitted_requests_sliced_at_end_date=submitted_requests_sliced_at_end_date, - start_date=start_date, - end_date=end_date, - ), + data={ + # Tracks what kind of orgs we are keeping count of. + # Used for the details table beneath the graph. + "org_count_types": [ + "Total", + "Federal", + "Interstate", + "State/Territory", + "Tribal", + "County", + "City", + "Special District", + "School District", + "Election Board", + ], + "user_count": models.User.objects.all().count(), + "domain_count": models.Domain.objects.all().count(), + "ready_domain_count": models.Domain.objects.filter(state=models.Domain.State.READY).count(), + "last_30_days_applications": last_30_days_applications.count(), + "last_30_days_approved_applications": last_30_days_approved_applications.count(), + "average_application_approval_time_last_30_days": avg_approval_time_display, + "managed_domains": { + "start_date_count": managed_domains_sliced_at_start_date, + "end_date_count": managed_domains_sliced_at_end_date, + }, + "unmanaged_domains": { + "start_date_count": unmanaged_domains_sliced_at_start_date, + "end_date_count": unmanaged_domains_sliced_at_end_date, + }, + "ready_domains": { + "start_date_count": ready_domains_sliced_at_start_date, + "end_date_count": ready_domains_sliced_at_end_date, + }, + "deleted_domains": { + "start_date_count": deleted_domains_sliced_at_start_date, + "end_date_count": deleted_domains_sliced_at_end_date, + }, + "requests": { + "start_date_count": requests_sliced_at_start_date, + "end_date_count": requests_sliced_at_end_date, + }, + "submitted_requests": { + "start_date_count": submitted_requests_sliced_at_start_date, + "end_date_count": submitted_requests_sliced_at_end_date, + }, + "start_date": start_date, + "end_date": end_date, + }, ) return render(request, "admin/analytics.html", context) From f539ede376626cb475aff9a6f42a48988436b16a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:53:27 -0700 Subject: [PATCH 086/139] fix style and andi stuff --- src/registrar/assets/src/sass/_theme/_admin.scss | 8 +++++++- src/registrar/templates/admin/analytics_graph_table.html | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index 4f75fd2fb..035768dad 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -536,9 +536,15 @@ details.dja-detail-table { background-color: transparent; } + thead tr { + background-color: var(--darkened-bg); + } + td, th { padding-left: 12px; - border: none + border: none; + background-color: var(--darkened-bg); + color: var(--body-quiet-color); } thead > tr > th { diff --git a/src/registrar/templates/admin/analytics_graph_table.html b/src/registrar/templates/admin/analytics_graph_table.html index 58132d023..88b538745 100644 --- a/src/registrar/templates/admin/analytics_graph_table.html +++ b/src/registrar/templates/admin/analytics_graph_table.html @@ -1,9 +1,9 @@ - +
- - - + + + From f154e9b870335fd6f8c31c755ad2e3f01b92cd1e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 7 Feb 2025 14:37:23 -0700 Subject: [PATCH 087/139] Add desktop / mobile view --- .../assets/src/sass/_theme/_admin.scss | 30 +- src/registrar/templates/admin/analytics.html | 529 +++++++++--------- 2 files changed, 303 insertions(+), 256 deletions(-) diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index 035768dad..05df9a3c2 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -548,7 +548,6 @@ details.dja-detail-table { } thead > tr > th { - border-radius: 4px; border-top: none; border-bottom: none; } @@ -930,3 +929,32 @@ ul.add-list-reset { background-color: transparent !important; } } + +@media (min-width: 1024px) { + .analytics-dashboard-charts { + // Desktop layout - charts in top row, details in bottom row + display: grid; + gap: 2rem; + grid-template-columns: 1fr 1fr; + grid-template-areas: + "chart1 chart2" + "details1 details2" + "chart3 chart4" + "details3 details4" + "chart5 chart6" + "details5 details6"; + + .chart-1 { grid-area: chart1; } + .details-1 { grid-area: details1; } + .chart-2 { grid-area: chart2; } + .details-2 { grid-area: details2; } + .chart-3 { grid-area: chart3; } + .details-3 { grid-area: details3; } + .chart-4 { grid-area: chart4; } + .details-4 { grid-area: details4; } + .chart-5 { grid-area: chart5; } + .details-5 { grid-area: details5; } + .chart-6 { grid-area: chart6; } + .details-6 { grid-area: details6; } + } +} diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index 014f948d1..297f27d46 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -1,258 +1,277 @@ {% extends "admin/base_site.html" %} {% load static %} - -{% block content_title %}

Registrar Analytics

{% endblock %} - -{% block content %} - -
- -
-
-
-

At a glance

-
-
    -
  • User Count: {{ data.user_count }}
  • -
  • Domain Count: {{ data.domain_count }}
  • -
  • Domains in READY state: {{ data.ready_domain_count }}
  • -
  • Domain applications (last 30 days): {{ data.last_30_days_applications }}
  • -
  • Approved applications (last 30 days): {{ data.last_30_days_approved_applications }}
  • -
  • Average approval time for applications (last 30 days): {{ data.average_application_approval_time_last_30_days }}
  • -
-
-
-
- -
- -
-
-
-

Growth reports

-
- {% comment %} - Inputs of type date suck for accessibility. - We'll need to replace those guys with a django form once we figure out how to hook one onto this page. - See the commit "Review for ticket #999" - {% endcomment %} -
-
- - -
-
- - -
-
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
- -
-
- -

Chart: Managed domains

-

{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

-
-
-
- -

Chart: Unmanaged domains

-

{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

-
-
-
- -
-
-
- Details for managed domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %} -
-
-
-
-
- Details for unmanaged domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %} -
-
-
-
- -
-
- -

Chart: Deleted domains

-

{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

-
-
-
- -

Chart: Ready domains

-

{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

-
-
-
- -
-
-
- Details for deleted domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %} -
-
-
-
-
- Details for ready domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %} -
-
-
-
- -
-
- -

Chart: Submitted requests

-

{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

-
-
-
- -

Chart: All requests

-

{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

-
-
-
- -
-
-
- Details for submitted requests -
- {% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %} -
-
-
-
-
- Details for all requests -
- {% include "admin/analytics_graph_table.html" with data=data property_name="requests" %} -
-
-
-
-
-
-
-
+{% block content_title %} +

Registrar Analytics

{% endblock %} +{% block content %} +
+
+
+
+

At a glance

+
+
    +
  • User Count: {{ data.user_count }}
  • +
  • Domain Count: {{ data.domain_count }}
  • +
  • Domains in READY state: {{ data.ready_domain_count }}
  • +
  • Domain applications (last 30 days): {{ data.last_30_days_applications }}
  • +
  • Approved applications (last 30 days): {{ data.last_30_days_approved_applications }}
  • +
  • Average approval time for applications (last 30 days): {{ data.average_application_approval_time_last_30_days }}
  • +
+
+
+
+ +
+
+
+
+

Growth reports

+
+ {% comment %} + Inputs of type date suck for accessibility. + We'll need to replace those guys with a django form once we figure out how to hook one onto this page. + See the commit "Review for ticket #999" + {% endcomment %} +
+
+ + +
+
+ + +
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ {% comment %} Managed/Unmanaged domains {% endcomment %} +
+
+ +

Chart: Managed domains

+

{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

+
+
+
+
+
+
+ Details for managed domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %} +
+
+
+
+
+
+ +

Chart: Unmanaged domains

+

{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

+
+
+
+
+
+
+ Details for unmanaged domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %} +
+
+
+
+ + {% comment %} Deleted/Ready domains {% endcomment %} +
+
+ +

Chart: Deleted domains

+

{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

+
+
+
+
+
+
+ Details for deleted domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %} +
+
+
+
+
+
+ +

Chart: Ready domains

+

{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

+
+
+
+
+
+
+ Details for ready domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %} +
+
+
+
+ + {% comment %} Requests {% endcomment %} +
+
+ +

Chart: Submitted requests

+

{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

+
+
+
+
+
+
+ Details for submitted requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %} +
+
+
+
+
+
+ +

Chart: All requests

+

{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

+
+
+
+
+
+
+ Details for all requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="requests" %} +
+
+
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file From d684fea8edbe091367b992d5bfad85630622df5c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:16:42 -0700 Subject: [PATCH 088/139] test changes --- src/registrar/assets/js/get-gov-reports.js | 35 +++++++++++++++--- .../assets/src/sass/_theme/_admin.scss | 15 ++++---- src/registrar/templates/admin/analytics.html | 36 +++++++++---------- 3 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index 8bfe32fdd..650a2c6c7 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -59,6 +59,8 @@ /** An IIFE to initialize the analytics page */ (function () { + // Store chart instances globally within this IIFE + const chartInstances = new Map(); function createComparativeColumnChart(canvasId, title, labelOne, labelTwo) { var canvas = document.getElementById(canvasId); if (!canvas) { @@ -80,17 +82,16 @@ borderWidth: 1, data: listOne, backgroundColor: [ - pattern.draw('zigzag-vertical', '#1f77b4'), + pattern.draw("zigzag-vertical", "#1f77b4"), ] }, { label: labelTwo, - backgroundColor: "rgba(75, 192, 192, 0.2)", borderColor: "rgba(75, 192, 192, 1)", borderWidth: 1, data: listTwo, backgroundColor: [ - pattern.draw('diagonal', '#1f77b4'), + pattern.draw("diagonal", "#1f77b4"), ] }, ], @@ -98,7 +99,6 @@ var options = { responsive: true, - maintainAspectRatio: false, plugins: { legend: { position: 'top', @@ -115,11 +115,34 @@ }, }; - new Chart(ctx, { + // Destroy existing chart instance if it exists + if (chartInstances.has(canvasId)) { + chartInstances.get(canvasId).destroy(); + } + + // Create and store new chart instance + const chart = new Chart(ctx, { type: "bar", data: data, options: options, }); + + chartInstances.set(canvasId, chart); + } + + function handleResize() { + // Debounce the resize handler + if (handleResize.timeout) { + clearTimeout(handleResize.timeout); + } + + handleResize.timeout = setTimeout(() => { + chartInstances.forEach((chart, canvasId) => { + if (chart && chart.canvas) { + chart.resize(); + } + }); + }, 100); } function initComparativeColumnCharts() { @@ -130,6 +153,8 @@ createComparativeColumnChart("myChart4", "Ready domains", "Start Date", "End Date"); createComparativeColumnChart("myChart5", "Submitted requests", "Start Date", "End Date"); createComparativeColumnChart("myChart6", "All requests", "Start Date", "End Date"); + + //window.addEventListener("resize", handleResize); }); }; diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index 05df9a3c2..ba7d447ee 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -930,11 +930,11 @@ ul.add-list-reset { } } -@media (min-width: 1024px) { +// Break at tablet view +@media (min-width: 768px) { .analytics-dashboard-charts { // Desktop layout - charts in top row, details in bottom row display: grid; - gap: 2rem; grid-template-columns: 1fr 1fr; grid-template-areas: "chart1 chart2" @@ -945,16 +945,17 @@ ul.add-list-reset { "details5 details6"; .chart-1 { grid-area: chart1; } - .details-1 { grid-area: details1; } .chart-2 { grid-area: chart2; } - .details-2 { grid-area: details2; } .chart-3 { grid-area: chart3; } - .details-3 { grid-area: details3; } .chart-4 { grid-area: chart4; } - .details-4 { grid-area: details4; } .chart-5 { grid-area: chart5; } - .details-5 { grid-area: details5; } .chart-6 { grid-area: chart6; } + .details-1 { grid-area: details1; } + .details-2 { grid-area: details2; } + .details-3 { grid-area: details3; } + .details-4 { grid-area: details4; } + .details-5 { grid-area: details5; } .details-6 { grid-area: details6; } } + } diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index 297f27d46..855f7f870 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -140,10 +140,10 @@
-
+
-
- Details for managed domains +
+ Details for managed domains
{% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %}
@@ -163,10 +163,10 @@
-
+
-
- Details for unmanaged domains +
+ Details for unmanaged domains
{% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %}
@@ -188,10 +188,10 @@
-
+
-
- Details for deleted domains +
+ Details for deleted domains
{% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %}
@@ -211,10 +211,10 @@
-
+
-
- Details for ready domains +
+ Details for ready domains
{% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %}
@@ -236,10 +236,10 @@
-
+
-
- Details for submitted requests +
+ Details for submitted requests
{% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %}
@@ -259,10 +259,10 @@
-
+
-
- Details for all requests +
+ Details for all requests
{% include "admin/analytics_graph_table.html" with data=data property_name="requests" %}
From e90e31acb713017ee587c1622b7f4425b6cacce9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:52:09 -0700 Subject: [PATCH 089/139] Closing in --- src/registrar/assets/js/get-gov-reports.js | 2 +- src/registrar/templates/admin/analytics.html | 226 +++++++++---------- 2 files changed, 102 insertions(+), 126 deletions(-) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index 650a2c6c7..7af40be45 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -154,7 +154,7 @@ createComparativeColumnChart("myChart5", "Submitted requests", "Start Date", "End Date"); createComparativeColumnChart("myChart6", "All requests", "Start Date", "End Date"); - //window.addEventListener("resize", handleResize); + window.addEventListener("resize", handleResize); }); }; diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index 855f7f870..4628ecfb4 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -127,147 +127,123 @@
{% comment %} Managed/Unmanaged domains {% endcomment %} -
-
- -

Chart: Managed domains

-

{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

-
-
+
+ +

Chart: Managed domains

+

{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

+
-
-
-
- Details for managed domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %} -
-
-
+
+
+ Details for managed domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %} +
+
-
- -

Chart: Unmanaged domains

-

{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

-
-
+ +

Chart: Unmanaged domains

+

{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

+
-
-
-
- Details for unmanaged domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %} -
-
-
+
+
+ Details for unmanaged domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %} +
+
{% comment %} Deleted/Ready domains {% endcomment %} -
-
- -

Chart: Deleted domains

-

{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

-
-
+
+ +

Chart: Deleted domains

+

{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

+
-
-
-
- Details for deleted domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %} -
-
-
+
+
+ Details for deleted domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %} +
+
-
-
- -

Chart: Ready domains

-

{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

-
-
+
+ +

Chart: Ready domains

+

{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

+
-
-
-
- Details for ready domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %} -
-
-
+
+
+ Details for ready domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %} +
+
{% comment %} Requests {% endcomment %} -
-
- -

Chart: Submitted requests

-

{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

-
-
+
+ +

Chart: Submitted requests

+

{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

+
-
-
-
- Details for submitted requests -
- {% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %} -
-
-
+
+
+ Details for submitted requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %} +
+
-
-
- -

Chart: All requests

-

{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

-
-
+
+ +

Chart: All requests

+

{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

+
-
-
-
- Details for all requests -
- {% include "admin/analytics_graph_table.html" with data=data property_name="requests" %} -
-
-
+
+
+ Details for all requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="requests" %} +
+
From 88d3a90521e1441e56e404b7fc93e2c91d22e771 Mon Sep 17 00:00:00 2001 From: Matt-Spence Date: Mon, 10 Feb 2025 11:56:01 -0500 Subject: [PATCH 090/139] linter errors --- .../management/commands/create_federal_portfolio.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 75c5d6acc..d753d0ce8 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -204,7 +204,9 @@ class Command(BaseCommand): # Fetch all users with manager roles for the domains # select_related means that a db query will not be occur when you do user_domain_role.user # Its similar to a set or dict in that it costs slightly more upfront in exchange for perf later - user_domain_roles = UserDomainRole.objects.select_related("user").filter(domain__in=domains, role=UserDomainRole.Roles.MANAGER) + user_domain_roles = UserDomainRole.objects.select_related("user").filter( + domain__in=domains, role=UserDomainRole.Roles.MANAGER + ) domain_managers.update(user_domain_roles) invited_managers: set[str] = set() @@ -243,9 +245,10 @@ class Command(BaseCommand): _, created = PortfolioInvitation.objects.get_or_create( portfolio=portfolio, email=email, - defaults={"status": PortfolioInvitation.PortfolioInvitationStatus.INVITED, - "roles": [UserPortfolioRoleChoices.ORGANIZATION_MEMBER] - }, + defaults={ + "status": PortfolioInvitation.PortfolioInvitationStatus.INVITED, + "roles": [UserPortfolioRoleChoices.ORGANIZATION_MEMBER], + }, ) if created: self.added_invitations.add(email) From e45f9f9525a92d34199916e250fd40cc9ef91a67 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:40:43 -0700 Subject: [PATCH 091/139] Fix uneven scaling --- src/registrar/assets/src/sass/_theme/_admin.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index ba7d447ee..c06c9c926 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -930,12 +930,12 @@ ul.add-list-reset { } } -// Break at tablet view -@media (min-width: 768px) { +@media (min-width: 1080px) { .analytics-dashboard-charts { // Desktop layout - charts in top row, details in bottom row display: grid; - grid-template-columns: 1fr 1fr; + // Equal columns each gets 1/2 of the space + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-areas: "chart1 chart2" "details1 details2" From 6d180eae71cf9a4a73c96b08ea69e5496a6ee3cf Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:08:58 -0700 Subject: [PATCH 092/139] Update analytics.html --- src/registrar/templates/admin/analytics.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index 4628ecfb4..b36511206 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -125,7 +125,7 @@ -
+
{% comment %} Managed/Unmanaged domains {% endcomment %}
Date: Mon, 10 Feb 2025 13:16:31 -0700 Subject: [PATCH 093/139] Adjustments from huddle --- .../assets/src/js/getgov/table-domains.js | 14 ++++++++------ src/registrar/assets/src/sass/_theme/_base.scss | 9 +++++++-- src/registrar/templates/portfolio_base.html | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/registrar/assets/src/js/getgov/table-domains.js b/src/registrar/assets/src/js/getgov/table-domains.js index 6536e7f6e..1cf1fc2dd 100644 --- a/src/registrar/assets/src/js/getgov/table-domains.js +++ b/src/registrar/assets/src/js/getgov/table-domains.js @@ -57,12 +57,14 @@ export class DomainsTable extends BaseTable { ${markupForSuborganizationRow}
`; tbody.appendChild(row); diff --git a/src/registrar/assets/src/sass/_theme/_base.scss b/src/registrar/assets/src/sass/_theme/_base.scss index f9418c542..904c0fb29 100644 --- a/src/registrar/assets/src/sass/_theme/_base.scss +++ b/src/registrar/assets/src/sass/_theme/_base.scss @@ -51,7 +51,7 @@ body { background-color: color('white'); border: 1px solid color('base-lighter'); border-radius: 4px; - padding: 0 units(4) units(3) units(2); + padding: 0 units(2) units(3) units(2); margin-top: units(3); &.margin-top-0 { @@ -276,6 +276,11 @@ abbr[title] { width: 25%; } +/* +NOTE: width: 3% basically forces a fit-content effect in the table. +Fit-content itself does not work. +*/ .width--action-column { - max-width: 80px; + width: 3%; + padding-right: 0px !important; } diff --git a/src/registrar/templates/portfolio_base.html b/src/registrar/templates/portfolio_base.html index 9f43c7251..ec7c0c22b 100644 --- a/src/registrar/templates/portfolio_base.html +++ b/src/registrar/templates/portfolio_base.html @@ -9,7 +9,7 @@ {# the entire logged in page goes here #}
-
+
{% block portfolio_content %}{% endblock %} From 77fedf4dc8265b019badddda767e3a02e54b6af5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:21:47 -0700 Subject: [PATCH 094/139] cleanup --- .../assets/src/sass/_theme/_admin.scss | 1 + src/registrar/templates/admin/analytics.html | 24 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index c06c9c926..07749d2dd 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -934,6 +934,7 @@ ul.add-list-reset { .analytics-dashboard-charts { // Desktop layout - charts in top row, details in bottom row display: grid; + gap: 2rem; // Equal columns each gets 1/2 of the space grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-areas: diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index b36511206..3747dc7b6 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -127,7 +127,7 @@
{% comment %} Managed/Unmanaged domains {% endcomment %} -
+
{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

-
+
Details for managed domains
@@ -146,7 +146,7 @@
-
+
{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

-
+
Details for unmanaged domains
@@ -167,7 +167,7 @@
{% comment %} Deleted/Ready domains {% endcomment %} -
+
{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

-
+
Details for deleted domains
@@ -186,7 +186,7 @@
-
+
{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

-
+
Details for ready domains
@@ -207,7 +207,7 @@
{% comment %} Requests {% endcomment %} -
+
{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

-
+
Details for submitted requests
@@ -226,7 +226,7 @@
-
+
{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

-
+
Details for all requests
From d9d751d6aede506125a2d61f539aa0831c660154 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:34:26 -0700 Subject: [PATCH 095/139] fix color --- src/registrar/assets/js/get-gov-reports.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index 8bfe32fdd..403a9c41e 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -75,22 +75,22 @@ datasets: [ { label: labelOne, - backgroundColor: "rgba(255, 99, 132, 0.2)", + backgroundColor: "rgba(255, 99, 132, 0.3)", borderColor: "rgba(255, 99, 132, 1)", borderWidth: 1, data: listOne, backgroundColor: [ - pattern.draw('zigzag-vertical', '#1f77b4'), + pattern.draw('diagonal-right-left', 'rgba(255, 99, 132, 0.3)'), ] }, { label: labelTwo, - backgroundColor: "rgba(75, 192, 192, 0.2)", + backgroundColor: "rgba(75, 192, 192, 0.3)", borderColor: "rgba(75, 192, 192, 1)", borderWidth: 1, data: listTwo, backgroundColor: [ - pattern.draw('diagonal', '#1f77b4'), + pattern.draw('diagonal', 'rgba(75, 192, 192, 0.3)'), ] }, ], From 16e251c4fc25ec985697de0d9269e249fea1a229 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 10 Feb 2025 12:47:53 -0800 Subject: [PATCH 096/139] Fix logic and additional unit tests --- src/registrar/admin.py | 12 +++- .../admin/change_form_object_tools.html | 26 ++++--- src/registrar/tests/test_admin_domain.py | 70 +++++++++++++++---- 3 files changed, 82 insertions(+), 26 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index b1b3d8adb..71c672dd7 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1519,8 +1519,7 @@ class DomainInvitationAdmin(BaseInvitationAdmin): change_form_template = "django/admin/domain_invitation_change_form.html" def change_view(self, request, object_id, form_url="", extra_context=None): - """Override the change_view to add the invitation obj for the - change_form_object_tools template""" + """Override the change_view to add the invitation obj for the change_form_object_tools template""" if extra_context is None: extra_context = {} @@ -1529,6 +1528,15 @@ class DomainInvitationAdmin(BaseInvitationAdmin): invitation = get_object_or_404(DomainInvitation, id=object_id) extra_context["invitation"] = invitation + if request.method == "POST" and "cancel_invitation" in request.POST: + if invitation.status == DomainInvitation.DomainInvitationStatus.INVITED: + invitation.cancel_invitation() + invitation.save(update_fields=["status"]) + messages.success(request, _("Invitation canceled successfully.")) + + # Redirect back to the change view + return redirect(reverse("admin:registrar_domaininvitation_change", args=[object_id])) + return super().change_view(request, object_id, form_url, extra_context) def save_model(self, request, obj, form, change): diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 541f4d162..a76609538 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -16,18 +16,22 @@ {% else %}
    {% if opts.model_name == 'domaininvitation' %} -
  • -
    - {% csrf_token %} - - -
  • + {% if invitation.status == invitation.DomainInvitationStatus.INVITED %} +
  • +
    + {% csrf_token %} + + + +
  • + {% endif %} {% endif %} +
  • {% translate "History" %} diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py index 07940e202..c192b082a 100644 --- a/src/registrar/tests/test_admin_domain.py +++ b/src/registrar/tests/test_admin_domain.py @@ -514,7 +514,7 @@ class DomainInvitationAdminTest(TestCase): self.client.force_login(self.staffuser) super().setUp() - def test_cancel_invitation_flow_in_admin(self): + def test_successful_cancel_invitation_flow_in_admin(self): """Testing canceling a domain invitation in Django Admin.""" # 1. Create a domain and assign staff user role + domain manager @@ -539,21 +539,65 @@ class DomainInvitationAdminTest(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, "Cancel invitation") - # 5. Click the "Cancel invitation" button (a POST) - cancel_invitation_url = reverse("invitation-cancel", args=[invitation.id]) - response = self.client.post(cancel_invitation_url, follow=True) + # 5. Click the cancel invitation button + response = self.client.post(domain_invitation_change_url, {"cancel_invitation": "true"}, follow=True) - # 6.Confirm we're redirected to the domain managers page for the domain - expected_redirect_url = reverse("domain-users", args=[domain.id]) - self.assertRedirects(response, expected_redirect_url) + # 6. Make sure we're redirect back to the change view page in /admin + self.assertRedirects(response, domain_invitation_change_url) - # 7. Get the messages - messages = list(get_messages(response.wsgi_request)) - message_texts = [str(message) for message in messages] + # 7. Confirm cancellation confirmation message appears + expected_message = f"Invitation for {invitation.email} on {domain.name} is canceled" + self.assertContains(response, expected_message) - # 8. Check that the success banner text is in the messages - expected_message = f"Canceled invitation to {invitation.email}." - self.assertIn(expected_message, message_texts) + def test_no_cancel_invitation_button_in_retrieved_state(self): + """Shouldn't be able to see the "Cancel invitation" button if invitation is RETRIEVED state""" + + # 1. Create a domain and assign staff user role + domain manager + domain = Domain.objects.create(name="retrieved.gov") + UserDomainRole.objects.create(user=self.staffuser, domain=domain, role="manager") + + # 2. Invite a domain manager to the above domain and NOT in invited state + invitation = DomainInvitation.objects.create( + email="retrievedinvitation@meoward.com", + domain=domain, + status=DomainInvitation.DomainInvitationStatus.RETRIEVED, + ) + + # 3. Go to the Domain Invitations list in /admin + domain_invitation_list_url = reverse("admin:registrar_domaininvitation_changelist") + response = self.client.get(domain_invitation_list_url) + self.assertEqual(response.status_code, 200) + + # 4. Go to the change view of that invitation and make sure you CANNOT see the button + domain_invitation_change_url = reverse("admin:registrar_domaininvitation_change", args=[invitation.id]) + response = self.client.get(domain_invitation_change_url) + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, "Cancel invitation") + + def test_no_cancel_invitation_button_in_canceled_state(self): + """Shouldn't be able to see the "Cancel invitation" button if invitation is CANCELED state""" + + # 1. Create a domain and assign staff user role + domain manager + domain = Domain.objects.create(name="canceled.gov") + UserDomainRole.objects.create(user=self.staffuser, domain=domain, role="manager") + + # 2. Invite a domain manager to the above domain and NOT in invited state + invitation = DomainInvitation.objects.create( + email="canceledinvitation@meoward.com", + domain=domain, + status=DomainInvitation.DomainInvitationStatus.CANCELED, + ) + + # 3. Go to the Domain Invitations list in /admin + domain_invitation_list_url = reverse("admin:registrar_domaininvitation_changelist") + response = self.client.get(domain_invitation_list_url) + self.assertEqual(response.status_code, 200) + + # 4. Go to the change view of that invitation and make sure you CANNOT see the button + domain_invitation_change_url = reverse("admin:registrar_domaininvitation_change", args=[invitation.id]) + response = self.client.get(domain_invitation_change_url) + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, "Cancel invitation") class TestDomainAdminWithClient(TestCase): From 8464cf7bdae63f633c6de27d0181193b0da0a45b Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 10 Feb 2025 12:54:52 -0800 Subject: [PATCH 097/139] Fix spacing --- src/registrar/admin.py | 8 ++++++++ .../django/admin/includes/email_clipboard_fieldset.html | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 71c672dd7..57590aaf5 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1518,6 +1518,14 @@ class DomainInvitationAdmin(BaseInvitationAdmin): change_form_template = "django/admin/domain_invitation_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) + def change_view(self, request, object_id, form_url="", extra_context=None): """Override the change_view to add the invitation obj for the change_form_object_tools template""" diff --git a/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html b/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html index 7907b5180..733fc3787 100644 --- a/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html +++ b/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html @@ -10,5 +10,4 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% else %} {{ block.super }} {% endif %} -{% endblock field_other %} - +{% endblock field_other %} \ No newline at end of file From 898168f44f53b8c19ab571b166592316dbbde193 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 10 Feb 2025 12:55:31 -0800 Subject: [PATCH 098/139] Remove extraneous comment --- src/registrar/admin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 57590aaf5..d1ca834d7 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1482,8 +1482,6 @@ class BaseInvitationAdmin(ListHeaderAdmin): class DomainInvitationAdmin(BaseInvitationAdmin): """Custom domain invitation admin class.""" - # form = DomainInvitationAdminForm - class Meta: model = models.DomainInvitation fields = "__all__" From 663e34fd5b6024b3f1306d981a7fc4b43e29dfcd Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 10 Feb 2025 13:02:43 -0800 Subject: [PATCH 099/139] Remove unused import for linting --- src/registrar/tests/test_admin_domain.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py index c192b082a..24ade9302 100644 --- a/src/registrar/tests/test_admin_domain.py +++ b/src/registrar/tests/test_admin_domain.py @@ -31,9 +31,6 @@ from .common import ( ) from unittest.mock import ANY, call, patch -from django.contrib.messages import get_messages - - import boto3_mocking # type: ignore import logging From e900255e681b35b3208bc85457d97bcbb4df25f9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:06:10 -0700 Subject: [PATCH 100/139] fix cutoff bug --- src/registrar/assets/js/get-gov-reports.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index 428b576ee..2cc8157b5 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -100,6 +100,7 @@ var options = { responsive: true, + maintainAspectRatio: false, plugins: { legend: { position: 'top', From 12fa9548a136521c910926994ee7e18cc53bd750 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:07:41 -0700 Subject: [PATCH 101/139] Update get-gov-reports.js --- src/registrar/assets/js/get-gov-reports.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index 2cc8157b5..d458ae05a 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -59,7 +59,6 @@ /** An IIFE to initialize the analytics page */ (function () { - // Store chart instances globally within this IIFE const chartInstances = new Map(); function createComparativeColumnChart(canvasId, title, labelOne, labelTwo) { var canvas = document.getElementById(canvasId); @@ -117,12 +116,10 @@ }, }; - // Destroy existing chart instance if it exists if (chartInstances.has(canvasId)) { chartInstances.get(canvasId).destroy(); } - // Create and store new chart instance const chart = new Chart(ctx, { type: "bar", data: data, From ce53ceac02be36409fef4f840376970b541e9b0c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 11 Feb 2025 08:56:28 -0700 Subject: [PATCH 102/139] Remove patternomaly --- src/registrar/assets/js/get-gov-reports.js | 82 +- src/registrar/assets/js/patternomaly.js | 1110 ------------------ src/registrar/templates/admin/base_site.html | 1 - 3 files changed, 76 insertions(+), 1117 deletions(-) delete mode 100644 src/registrar/assets/js/patternomaly.js diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index 403a9c41e..65608ede9 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -59,6 +59,80 @@ /** An IIFE to initialize the analytics page */ (function () { + + // This code is adapted from here: + // https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns + function createDiagonalPattern(backgroundColor, lineColor="white") { + // create a 10x10 px canvas for the pattern's base shape + let shape = document.createElement("canvas") + shape.width = 20 + shape.height = 20 + // get the context for drawing + let c = shape.getContext("2d") + + // Fill with specified background color + c.fillStyle = backgroundColor + c.fillRect(0, 0, shape.width, shape.height) + + // Set stroke properties + c.strokeStyle = lineColor + c.lineWidth = 2 + + // Draw diagonal lines similarly to the patternomaly library + c.beginPath() + + // First diagonal line + let halfSize = shape.width / 2 + let gap = 1 + + c.moveTo(halfSize - gap, -gap) + c.lineTo(shape.width + 1, halfSize + 1) + + // Second diagonal line (offset) + c.moveTo(halfSize - gap - halfSize, halfSize - gap) + c.lineTo(shape.width + 1 - halfSize, halfSize + 1 + halfSize) + + c.stroke() + + return c.createPattern(shape, "repeat") + } + + function createDiagonalRightLeftPattern(backgroundColor, lineColor="white") { + // create a 20x20 px canvas for larger pattern repeat + let shape = document.createElement("canvas") + shape.width = 20 + shape.height = 20 + // get the context for drawing + let c = shape.getContext("2d") + + // Fill with specified background color + c.fillStyle = backgroundColor + c.fillRect(0, 0, shape.width, shape.height) + + // Translate and rotate context + c.translate(shape.width, 0) + c.rotate(90 * Math.PI / 180) + + // Set stroke properties + c.strokeStyle = lineColor + c.lineWidth = 2 + + // First diagonal line + let halfSize = shape.width / 2 + let gap = 1 + + c.moveTo(halfSize - gap, -gap) + c.lineTo(shape.width + 1, halfSize + 1) + + // Second diagonal line (offset) + c.moveTo(halfSize - gap - halfSize, halfSize - gap) + c.lineTo(shape.width + 1 - halfSize, halfSize + 1 + halfSize) + + c.stroke() + + return c.createPattern(shape, "repeat") + } + function createComparativeColumnChart(canvasId, title, labelOne, labelTwo) { var canvas = document.getElementById(canvasId); if (!canvas) { @@ -79,9 +153,7 @@ borderColor: "rgba(255, 99, 132, 1)", borderWidth: 1, data: listOne, - backgroundColor: [ - pattern.draw('diagonal-right-left', 'rgba(255, 99, 132, 0.3)'), - ] + backgroundColor: createDiagonalRightLeftPattern('rgba(255, 99, 132, 0.3)') }, { label: labelTwo, @@ -89,9 +161,7 @@ borderColor: "rgba(75, 192, 192, 1)", borderWidth: 1, data: listTwo, - backgroundColor: [ - pattern.draw('diagonal', 'rgba(75, 192, 192, 0.3)'), - ] + backgroundColor: createDiagonalPattern('rgba(75, 192, 192, 0.3)') }, ], }; diff --git a/src/registrar/assets/js/patternomaly.js b/src/registrar/assets/js/patternomaly.js deleted file mode 100644 index e349db0f0..000000000 --- a/src/registrar/assets/js/patternomaly.js +++ /dev/null @@ -1,1110 +0,0 @@ -// This file is copied from the patternomaly library for chart.js. -// This allows us to add patterns for better accessibility for color-blind users. -// Source: https://github.com/ashiguruma/patternomaly - -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global.pattern = factory()); -}(this, (function () { 'use strict'; - -var BACKGROUND_COLOR = 'rgba(100, 100, 100, 0.7)'; -var PATTERN_COLOR = 'rgba(255, 255, 255, 0.8)'; -var POINT_STYLE = 'round'; - -var asyncGenerator = function () { - function AwaitValue(value) { - this.value = value; - } - - function AsyncGenerator(gen) { - var front, back; - - function send(key, arg) { - return new Promise(function (resolve, reject) { - var request = { - key: key, - arg: arg, - resolve: resolve, - reject: reject, - next: null - }; - - if (back) { - back = back.next = request; - } else { - front = back = request; - resume(key, arg); - } - }); - } - - function resume(key, arg) { - try { - var result = gen[key](arg); - var value = result.value; - - if (value instanceof AwaitValue) { - Promise.resolve(value.value).then(function (arg) { - resume("next", arg); - }, function (arg) { - resume("throw", arg); - }); - } else { - settle(result.done ? "return" : "normal", result.value); - } - } catch (err) { - settle("throw", err); - } - } - - function settle(type, value) { - switch (type) { - case "return": - front.resolve({ - value: value, - done: true - }); - break; - - case "throw": - front.reject(value); - break; - - default: - front.resolve({ - value: value, - done: false - }); - break; - } - - front = front.next; - - if (front) { - resume(front.key, front.arg); - } else { - back = null; - } - } - - this._invoke = send; - - if (typeof gen.return !== "function") { - this.return = undefined; - } - } - - if (typeof Symbol === "function" && Symbol.asyncIterator) { - AsyncGenerator.prototype[Symbol.asyncIterator] = function () { - return this; - }; - } - - AsyncGenerator.prototype.next = function (arg) { - return this._invoke("next", arg); - }; - - AsyncGenerator.prototype.throw = function (arg) { - return this._invoke("throw", arg); - }; - - AsyncGenerator.prototype.return = function (arg) { - return this._invoke("return", arg); - }; - - return { - wrap: function (fn) { - return function () { - return new AsyncGenerator(fn.apply(this, arguments)); - }; - }, - await: function (value) { - return new AwaitValue(value); - } - }; -}(); - -var classCallCheck = function (instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } -}; - -var createClass = function () { - function defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } - } - - return function (Constructor, protoProps, staticProps) { - if (protoProps) defineProperties(Constructor.prototype, protoProps); - if (staticProps) defineProperties(Constructor, staticProps); - return Constructor; - }; -}(); - -var _extends = Object.assign || function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key]; - } - } - } - - return target; -}; - -var inherits = function (subClass, superClass) { - if (typeof superClass !== "function" && superClass !== null) { - throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); - } - - subClass.prototype = Object.create(superClass && superClass.prototype, { - constructor: { - value: subClass, - enumerable: false, - writable: true, - configurable: true - } - }); - if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; -}; - -var possibleConstructorReturn = function (self, call) { - if (!self) { - throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - } - - return call && (typeof call === "object" || typeof call === "function") ? call : self; -}; - -var Shape = function () { - function Shape() { - var size = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 20; - var backgroundColor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : BACKGROUND_COLOR; - var patternColor = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : PATTERN_COLOR; - classCallCheck(this, Shape); - - this._canvas = document.createElement('canvas'); - this._context = this._canvas.getContext('2d'); - - this._canvas.width = size; - this._canvas.height = size; - - this._context.fillStyle = backgroundColor; - this._context.fillRect(0, 0, this._canvas.width, this._canvas.height); - - this._size = size; - this._patternColor = patternColor; - - return this; - } - - createClass(Shape, [{ - key: 'setStrokeProps', - value: function setStrokeProps() { - this._context.strokeStyle = this._patternColor; - this._context.lineWidth = this._size / 10; - this._context.lineJoin = POINT_STYLE; - this._context.lineCap = POINT_STYLE; - } - }, { - key: 'setFillProps', - value: function setFillProps() { - this._context.fillStyle = this._patternColor; - } - }]); - return Shape; -}(); - -var Plus = function (_Shape) { - inherits(Plus, _Shape); - - function Plus() { - classCallCheck(this, Plus); - return possibleConstructorReturn(this, (Plus.__proto__ || Object.getPrototypeOf(Plus)).apply(this, arguments)); - } - - createClass(Plus, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - - this._context.beginPath(); - - this.setStrokeProps(); - - this.drawPlus(); - this.drawPlus(halfSize, halfSize); - - this._context.stroke(); - - return this._canvas; - } - }, { - key: 'drawPlus', - value: function drawPlus() { - var offsetX = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var offsetY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - - var size = this._size; - var halfSize = size / 2; - var quarterSize = size / 4; - - this._context.moveTo(quarterSize + offsetX, 0 + offsetY); - this._context.lineTo(quarterSize + offsetX, halfSize + offsetY); - this._context.moveTo(0 + offsetX, quarterSize + offsetY); - this._context.lineTo(halfSize + offsetX, quarterSize + offsetY); - - this._context.closePath(); - } - }]); - return Plus; -}(Shape); - -var Cross = function (_Shape) { - inherits(Cross, _Shape); - - function Cross() { - classCallCheck(this, Cross); - return possibleConstructorReturn(this, (Cross.__proto__ || Object.getPrototypeOf(Cross)).apply(this, arguments)); - } - - createClass(Cross, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - - this._context.beginPath(); - - this.setStrokeProps(); - - this.drawCross(); - this.drawCross(halfSize, halfSize); - - this._context.stroke(); - - return this._canvas; - } - }, { - key: 'drawCross', - value: function drawCross() { - var offsetX = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var offsetY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - - var size = this._size; - var halfSize = size / 2; - var gap = 2; - - this._context.moveTo(offsetX + gap, offsetY + gap); - this._context.lineTo(halfSize - gap + offsetX, halfSize - gap + offsetY); - this._context.moveTo(offsetX + gap, halfSize - gap + offsetY); - this._context.lineTo(halfSize - gap + offsetX, offsetY + gap); - - this._context.closePath(); - } - }]); - return Cross; -}(Shape); - -var Dash = function (_Shape) { - inherits(Dash, _Shape); - - function Dash() { - classCallCheck(this, Dash); - return possibleConstructorReturn(this, (Dash.__proto__ || Object.getPrototypeOf(Dash)).apply(this, arguments)); - } - - createClass(Dash, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - - this._context.beginPath(); - - this.setStrokeProps(); - - this.drawDash(); - this.drawDash(halfSize, halfSize); - - this._context.stroke(); - - return this._canvas; - } - }, { - key: 'drawDash', - value: function drawDash() { - var offsetX = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var offsetY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - - var size = this._size; - var halfSize = size / 2; - var gap = 2; - - this._context.moveTo(offsetX + gap, offsetY + gap); - this._context.lineTo(halfSize - gap + offsetX, halfSize - gap + offsetY); - - this._context.closePath(); - } - }]); - return Dash; -}(Shape); - -var CrossDash = function (_Shape) { - inherits(CrossDash, _Shape); - - function CrossDash() { - classCallCheck(this, CrossDash); - return possibleConstructorReturn(this, (CrossDash.__proto__ || Object.getPrototypeOf(CrossDash)).apply(this, arguments)); - } - - createClass(CrossDash, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - this._context.beginPath(); - - this.setStrokeProps(); - - var cross = new Cross(); - cross.drawCross.call(this); - - var dash = new Dash(); - dash.drawDash.call(this, halfSize, halfSize); - - this._context.stroke(); - - return this._canvas; - } - }]); - return CrossDash; -}(Shape); - -var Dot = function (_Shape) { - inherits(Dot, _Shape); - - function Dot() { - classCallCheck(this, Dot); - return possibleConstructorReturn(this, (Dot.__proto__ || Object.getPrototypeOf(Dot)).apply(this, arguments)); - } - - createClass(Dot, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - - this._context.beginPath(); - - this.setFillProps(); - - this.drawDot(); - this.drawDot(halfSize, halfSize); - - this._context.fill(); - - return this._canvas; - } - }, { - key: 'drawDot', - value: function drawDot() { - var offsetX = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var offsetY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - var diameter = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this._size / 10; - - var size = this._size; - var quarterSize = size / 4; - var x = quarterSize + offsetX; - var y = quarterSize + offsetY; - - this._context.moveTo(x + quarterSize, y); - this._context.arc(x, y, diameter, 0, 2 * Math.PI); - - this._context.closePath(); - } - }]); - return Dot; -}(Shape); - -var DotDash = function (_Shape) { - inherits(DotDash, _Shape); - - function DotDash() { - classCallCheck(this, DotDash); - return possibleConstructorReturn(this, (DotDash.__proto__ || Object.getPrototypeOf(DotDash)).apply(this, arguments)); - } - - createClass(DotDash, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - - this._context.beginPath(); - - this.setStrokeProps(); - - var dash = new Dash(); - dash.drawDash.call(this, halfSize, halfSize); - - this._context.closePath(); - this._context.stroke(); - - this.setFillProps(); - - var dot = new Dot(); - dot.drawDot.call(this); - - this._context.fill(); - - return this._canvas; - } - }]); - return DotDash; -}(Shape); - -var Disc = function (_Dot) { - inherits(Disc, _Dot); - - function Disc() { - classCallCheck(this, Disc); - return possibleConstructorReturn(this, (Disc.__proto__ || Object.getPrototypeOf(Disc)).apply(this, arguments)); - } - - createClass(Disc, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - var diameter = this._size / 5; - - this._context.beginPath(); - - this.setFillProps(); - - this.drawDot(0, 0, diameter); - this.drawDot(halfSize, halfSize, diameter); - - this._context.fill(); - - return this._canvas; - } - }]); - return Disc; -}(Dot); - -var Ring = function (_Dot) { - inherits(Ring, _Dot); - - function Ring() { - classCallCheck(this, Ring); - return possibleConstructorReturn(this, (Ring.__proto__ || Object.getPrototypeOf(Ring)).apply(this, arguments)); - } - - createClass(Ring, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - var diameter = this._size / 5; - - this._context.beginPath(); - - this.setStrokeProps(); - - this.drawDot(0, 0, diameter); - this.drawDot(halfSize, halfSize, diameter); - - this._context.stroke(); - - return this._canvas; - } - }]); - return Ring; -}(Dot); - -var Line = function (_Shape) { - inherits(Line, _Shape); - - function Line() { - classCallCheck(this, Line); - return possibleConstructorReturn(this, (Line.__proto__ || Object.getPrototypeOf(Line)).apply(this, arguments)); - } - - createClass(Line, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - - this._context.beginPath(); - - this.setStrokeProps(); - - this.drawLine(); - this.drawLine(halfSize, halfSize); - - this._context.stroke(); - - return this._canvas; - } - }, { - key: 'drawLine', - value: function drawLine() { - var offsetX = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var offsetY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - - var size = this._size; - var quarterSize = size / 4; - - this._context.moveTo(0, quarterSize + offsetY); - this._context.lineTo(this._size, quarterSize + offsetY); - - this._context.closePath(); - } - }]); - return Line; -}(Shape); - -var VerticalLine = function (_Line) { - inherits(VerticalLine, _Line); - - function VerticalLine() { - classCallCheck(this, VerticalLine); - return possibleConstructorReturn(this, (VerticalLine.__proto__ || Object.getPrototypeOf(VerticalLine)).apply(this, arguments)); - } - - createClass(VerticalLine, [{ - key: 'drawTile', - value: function drawTile() { - this._context.translate(this._size, 0); - this._context.rotate(90 * Math.PI / 180); - - Line.prototype.drawTile.call(this); - - return this._canvas; - } - }]); - return VerticalLine; -}(Line); - -var Weave = function (_Shape) { - inherits(Weave, _Shape); - - function Weave() { - classCallCheck(this, Weave); - return possibleConstructorReturn(this, (Weave.__proto__ || Object.getPrototypeOf(Weave)).apply(this, arguments)); - } - - createClass(Weave, [{ - key: 'drawTile', - value: function drawTile() { - this._context.beginPath(); - - this.setStrokeProps(); - - this.drawWeave(0, 0); - - this._context.stroke(); - - return this._canvas; - } - }, { - key: 'drawWeave', - value: function drawWeave(offsetX, offsetY) { - var size = this._size; - var halfSize = size / 2; - - this._context.moveTo(offsetX + 1, offsetY + 1); - this._context.lineTo(halfSize - 1, halfSize - 1); - - this._context.moveTo(halfSize + 1, size - 1); - this._context.lineTo(size - 1, halfSize + 1); - - this._context.closePath(); - } - }]); - return Weave; -}(Shape); - -var Zigzag = function (_Shape) { - inherits(Zigzag, _Shape); - - function Zigzag() { - classCallCheck(this, Zigzag); - return possibleConstructorReturn(this, (Zigzag.__proto__ || Object.getPrototypeOf(Zigzag)).apply(this, arguments)); - } - - createClass(Zigzag, [{ - key: 'drawTile', - value: function drawTile() { - this._context.beginPath(); - - this.setStrokeProps(); - - this.drawZigzag(); - this.drawZigzag(this._size / 2); - - this._context.stroke(); - - return this._canvas; - } - }, { - key: 'drawZigzag', - value: function drawZigzag() { - var offsetY = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - - var size = this._size; - var quarterSize = size / 4; - var halfSize = size / 2; - var tenthSize = size / 10; - - this._context.moveTo(0, tenthSize + offsetY); - this._context.lineTo(quarterSize, halfSize - tenthSize + offsetY); - this._context.lineTo(halfSize, tenthSize + offsetY); - this._context.lineTo(size - quarterSize, halfSize - tenthSize + offsetY); - this._context.lineTo(size, tenthSize + offsetY); - } - }]); - return Zigzag; -}(Shape); - -var ZigzagVertical = function (_Zigzag) { - inherits(ZigzagVertical, _Zigzag); - - function ZigzagVertical() { - classCallCheck(this, ZigzagVertical); - return possibleConstructorReturn(this, (ZigzagVertical.__proto__ || Object.getPrototypeOf(ZigzagVertical)).apply(this, arguments)); - } - - createClass(ZigzagVertical, [{ - key: 'drawTile', - value: function drawTile() { - this._context.translate(this._size, 0); - this._context.rotate(90 * Math.PI / 180); - - Zigzag.prototype.drawTile.call(this); - - return this._canvas; - } - }]); - return ZigzagVertical; -}(Zigzag); - -var Diagonal = function (_Shape) { - inherits(Diagonal, _Shape); - - function Diagonal() { - classCallCheck(this, Diagonal); - return possibleConstructorReturn(this, (Diagonal.__proto__ || Object.getPrototypeOf(Diagonal)).apply(this, arguments)); - } - - createClass(Diagonal, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - - this._context.beginPath(); - - this.setStrokeProps(); - - this.drawDiagonalLine(); - this.drawDiagonalLine(halfSize, halfSize); - - this._context.stroke(); - - return this._canvas; - } - }, { - key: 'drawDiagonalLine', - value: function drawDiagonalLine() { - var offsetX = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var offsetY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - - var size = this._size; - var halfSize = size / 2; - var gap = 1; - - this._context.moveTo(halfSize - gap - offsetX, gap * -1 + offsetY); - this._context.lineTo(size + 1 - offsetX, halfSize + 1 + offsetY); - - this._context.closePath(); - } - }]); - return Diagonal; -}(Shape); - -var DiagonalRightLeft = function (_Diagonal) { - inherits(DiagonalRightLeft, _Diagonal); - - function DiagonalRightLeft() { - classCallCheck(this, DiagonalRightLeft); - return possibleConstructorReturn(this, (DiagonalRightLeft.__proto__ || Object.getPrototypeOf(DiagonalRightLeft)).apply(this, arguments)); - } - - createClass(DiagonalRightLeft, [{ - key: 'drawTile', - value: function drawTile() { - this._context.translate(this._size, 0); - this._context.rotate(90 * Math.PI / 180); - - Diagonal.prototype.drawTile.call(this); - - return this._canvas; - } - }]); - return DiagonalRightLeft; -}(Diagonal); - -var Square = function (_Shape) { - inherits(Square, _Shape); - - function Square() { - classCallCheck(this, Square); - return possibleConstructorReturn(this, (Square.__proto__ || Object.getPrototypeOf(Square)).apply(this, arguments)); - } - - createClass(Square, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - - this._context.beginPath(); - - this.setFillProps(); - - this.drawSquare(); - this.drawSquare(halfSize, halfSize); - - this._context.fill(); - - return this._canvas; - } - }, { - key: 'drawSquare', - value: function drawSquare() { - var offsetX = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var offsetY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - - var size = this._size; - var halfSize = size / 2; - var gap = size / 20; - - this._context.fillRect(offsetX + gap, offsetY + gap, halfSize - gap * 2, halfSize - gap * 2); - - this._context.closePath(); - } - }]); - return Square; -}(Shape); - -var Box = function (_Shape) { - inherits(Box, _Shape); - - function Box() { - classCallCheck(this, Box); - return possibleConstructorReturn(this, (Box.__proto__ || Object.getPrototypeOf(Box)).apply(this, arguments)); - } - - createClass(Box, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - - this._context.beginPath(); - - this.setStrokeProps(); - - this.drawBox(); - this.drawBox(halfSize, halfSize); - - this._context.stroke(); - - return this._canvas; - } - }, { - key: 'drawBox', - value: function drawBox() { - var offsetX = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var offsetY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - - var size = this._size; - var halfSize = size / 2; - var gap = size / 20; - - this._context.strokeRect(offsetX + gap, offsetY + gap, halfSize - gap * 4, halfSize - gap * 4); - - this._context.closePath(); - } - }]); - return Box; -}(Shape); - -var Triangle = function (_Shape) { - inherits(Triangle, _Shape); - - function Triangle() { - classCallCheck(this, Triangle); - return possibleConstructorReturn(this, (Triangle.__proto__ || Object.getPrototypeOf(Triangle)).apply(this, arguments)); - } - - createClass(Triangle, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - - this._context.beginPath(); - - this.setFillProps(); - - this.drawTriangle(); - this.drawTriangle(halfSize, halfSize); - - this._context.fill(); - - return this._canvas; - } - }, { - key: 'drawTriangle', - value: function drawTriangle() { - var offsetX = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var offsetY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - - var size = this._size; - var halfSize = size / 2; - var quarterSize = size / 4; - - this._context.moveTo(quarterSize + offsetX, offsetY); - this._context.lineTo(halfSize + offsetX, halfSize + offsetY); - this._context.lineTo(offsetX, halfSize + offsetY); - - this._context.closePath(); - } - }]); - return Triangle; -}(Shape); - -var TriangleVertical = function (_Triangle) { - inherits(TriangleVertical, _Triangle); - - function TriangleVertical() { - classCallCheck(this, TriangleVertical); - return possibleConstructorReturn(this, (TriangleVertical.__proto__ || Object.getPrototypeOf(TriangleVertical)).apply(this, arguments)); - } - - createClass(TriangleVertical, [{ - key: 'drawTile', - value: function drawTile() { - var size = this._size; - - this._context.translate(size, size); - this._context.rotate(180 * Math.PI / 180); - - Triangle.prototype.drawTile.call(this); - - return this._canvas; - } - }]); - return TriangleVertical; -}(Triangle); - -var Diamond = function (_Shape) { - inherits(Diamond, _Shape); - - function Diamond() { - classCallCheck(this, Diamond); - return possibleConstructorReturn(this, (Diamond.__proto__ || Object.getPrototypeOf(Diamond)).apply(this, arguments)); - } - - createClass(Diamond, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - - this._context.beginPath(); - - this.setFillProps(); - - this.drawDiamond(); - this.drawDiamond(halfSize, halfSize); - - this._context.fill(); - - return this._canvas; - } - }, { - key: 'drawDiamond', - value: function drawDiamond() { - var offsetX = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var offsetY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - - var size = this._size; - var halfSize = size / 2; - var quarterSize = size / 4; - - this._context.moveTo(quarterSize + offsetX, offsetY); - this._context.lineTo(halfSize + offsetX, quarterSize + offsetY); - this._context.lineTo(quarterSize + offsetX, halfSize + offsetY); - this._context.lineTo(offsetX, quarterSize + offsetY); - - this._context.closePath(); - } - }]); - return Diamond; -}(Shape); - -var DiamondBox = function (_Diamond) { - inherits(DiamondBox, _Diamond); - - function DiamondBox() { - classCallCheck(this, DiamondBox); - return possibleConstructorReturn(this, (DiamondBox.__proto__ || Object.getPrototypeOf(DiamondBox)).apply(this, arguments)); - } - - createClass(DiamondBox, [{ - key: 'drawTile', - value: function drawTile() { - var halfSize = this._size / 2; - - this._context.beginPath(); - - this.setStrokeProps(); - - this.drawDiamond(); - this.drawDiamond(halfSize, halfSize); - - this._context.stroke(); - - return this._canvas; - } - }, { - key: 'drawDiamond', - value: function drawDiamond() { - var offsetX = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var offsetY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - - var size = this._size; - var halfSize = size / 2 - 1; - var quarterSize = size / 4; - - this._context.moveTo(quarterSize + offsetX, offsetY + 1); - this._context.lineTo(halfSize + offsetX, quarterSize + offsetY); - this._context.lineTo(quarterSize + offsetX, halfSize + offsetY); - this._context.lineTo(offsetX + 1, quarterSize + offsetY); - - this._context.closePath(); - } - }]); - return DiamondBox; -}(Diamond); - -var shapes = { - 'plus': Plus, - 'cross': Cross, - 'dash': Dash, - 'cross-dash': CrossDash, - 'dot': Dot, - 'dot-dash': DotDash, - 'disc': Disc, - 'ring': Ring, - 'line': Line, - 'line-vertical': VerticalLine, - 'weave': Weave, - 'zigzag': Zigzag, - 'zigzag-vertical': ZigzagVertical, - 'diagonal': Diagonal, - 'diagonal-right-left': DiagonalRightLeft, - 'square': Square, - 'box': Box, - 'triangle': Triangle, - 'triangle-inverted': TriangleVertical, - 'diamond': Diamond, - 'diamond-box': DiamondBox -}; - -var deprecatedShapes = { - 'circle': shapes['disc'], - 'triangle-vertical': shapes['triangle-inverted'], - 'line-horizontal': shapes['line'], - 'line-diagonal-lr': shapes['diagonal'], - 'line-diagonal-rl': shapes['diagonal-right-left'], - 'zigzag-horizontal': shapes['zigzag'], - 'diamond-outline': shapes['diamond-box'] -}; - -var completeShapesList = []; - -function getRandomShape() { - var excludedShapeTypes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; - - var shapesList = Object.keys(shapes); - - excludedShapeTypes.forEach(function (shapeType) { - shapesList.splice(shapesList.indexOf(shapeType), 1); - }); - - var randomIndex = Math.floor(Math.random() * shapesList.length); - - return shapesList[randomIndex]; -} - -_extends(completeShapesList, shapes, deprecatedShapes); - -function draw() { - var shapeType = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'square'; - var backgroundColor = arguments[1]; - var patternColor = arguments[2]; - var size = arguments[3]; - - var patternCanvas = document.createElement('canvas'); - var patternContext = patternCanvas.getContext('2d'); - var outerSize = size * 2; - - var Shape = completeShapesList[shapeType]; - var shape = new Shape(size, backgroundColor, patternColor); - - var pattern = patternContext.createPattern(shape.drawTile(), 'repeat'); - - patternCanvas.width = outerSize; - patternCanvas.height = outerSize; - - pattern.shapeType = shapeType; - - return pattern; -} - -function generate(colorList) { - var firstShapeType = void 0; - var previousShapeType = void 0; - - return colorList.map(function (color, index, list) { - var shapeType = void 0; - - if (index === 0) { - shapeType = getRandomShape(); - previousShapeType = shapeType; - firstShapeType = previousShapeType; - } else if (index === list.length - 1) { - shapeType = getRandomShape([previousShapeType, firstShapeType]); - } else { - shapeType = getRandomShape([previousShapeType]); - previousShapeType = shapeType; - } - - return draw(shapeType, color); - }); -} - -var pattern = { - draw: draw, - generate: generate -}; - -return pattern; - -}))); -//# sourceMappingURL=patternomaly.js.map diff --git a/src/registrar/templates/admin/base_site.html b/src/registrar/templates/admin/base_site.html index d7c43647a..b80917bb2 100644 --- a/src/registrar/templates/admin/base_site.html +++ b/src/registrar/templates/admin/base_site.html @@ -23,7 +23,6 @@ - {% endblock %} From 4ee1356dac25d2fb87b501ee0ac155595ef3c03b Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Tue, 11 Feb 2025 10:06:36 -0600 Subject: [PATCH 103/139] update documentation --- docs/operations/data_migration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/operations/data_migration.md b/docs/operations/data_migration.md index cdef3dba7..8185922a4 100644 --- a/docs/operations/data_migration.md +++ b/docs/operations/data_migration.md @@ -914,7 +914,8 @@ Example (only requests): `./manage.py create_federal_portfolio --branch "executi | 3 | **both** | If True, runs parse_requests and parse_domains. | | 4 | **parse_requests** | If True, then the created portfolio is added to all related DomainRequests. | | 5 | **parse_domains** | If True, then the created portfolio is added to all related Domains. | -| 6 | **skip_existing_portfolios** | If True, then the script will only create suborganizations, modify DomainRequest, and modify DomainInformation records only when creating a new portfolio. Use this flag when you do not want to modify existing records. | +| 6 | **add_managers** | If True, then the created portfolio will add all managers of the portfolio domains as members of the portfolio, including invited managers. | +| 7 | **skip_existing_portfolios** | If True, then the script will only create suborganizations, modify DomainRequest, and modify DomainInformation records only when creating a new portfolio. Use this flag when you do not want to modify existing records. | - Parameters #1-#2: Either `--agency_name` or `--branch` must be specified. Not both. - Parameters #2-#3, you cannot use `--both` while using these. You must specify either `--parse_requests` or `--parse_domains` seperately. While all of these parameters are optional in that you do not need to specify all of them, From b873716f0cb73d8b2ab0e3ac6e1f21bf61224928 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 11 Feb 2025 09:07:38 -0700 Subject: [PATCH 104/139] Add proper credit --- src/registrar/assets/js/get-gov-reports.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index 65608ede9..c3c5b3c0b 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -60,8 +60,10 @@ */ (function () { - // This code is adapted from here: + // These functions are adapted from here: // https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns + // Additionally, code is also adapted from the patternomaly library: + // https://github.com/ashiguruma/patternomaly function createDiagonalPattern(backgroundColor, lineColor="white") { // create a 10x10 px canvas for the pattern's base shape let shape = document.createElement("canvas") From 07c7f58f205d0f87b35b1cb68ecba27bf33a5534 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:17:26 -0700 Subject: [PATCH 105/139] Cleanup code --- src/registrar/assets/js/get-gov-reports.js | 112 ++++++++------------- 1 file changed, 41 insertions(+), 71 deletions(-) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index c3c5b3c0b..382e91fc6 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -60,79 +60,48 @@ */ (function () { - // These functions are adapted from here: - // https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns - // Additionally, code is also adapted from the patternomaly library: - // https://github.com/ashiguruma/patternomaly - function createDiagonalPattern(backgroundColor, lineColor="white") { - // create a 10x10 px canvas for the pattern's base shape - let shape = document.createElement("canvas") - shape.width = 20 - shape.height = 20 - // get the context for drawing - let c = shape.getContext("2d") - - // Fill with specified background color - c.fillStyle = backgroundColor - c.fillRect(0, 0, shape.width, shape.height) - - // Set stroke properties - c.strokeStyle = lineColor - c.lineWidth = 2 - - // Draw diagonal lines similarly to the patternomaly library - c.beginPath() - - // First diagonal line - let halfSize = shape.width / 2 - let gap = 1 - - c.moveTo(halfSize - gap, -gap) - c.lineTo(shape.width + 1, halfSize + 1) - - // Second diagonal line (offset) - c.moveTo(halfSize - gap - halfSize, halfSize - gap) - c.lineTo(shape.width + 1 - halfSize, halfSize + 1 + halfSize) - - c.stroke() - - return c.createPattern(shape, "repeat") - } + /** + * Creates a diagonal stripe pattern for chart.js + * Inspired by https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns + * and https://github.com/ashiguruma/patternomaly + * @param {string} [lineColor="white"] - Color of the diagonal lines + * @param {string} backgroundColor - Background color of the pattern + * @param {boolean} [rightToLeft=false] - Direction of the diagonal lines + * @param {number} [lineGap=1] - Gap between lines + * @returns {CanvasPattern} A canvas pattern object for use with backgroundColor + */ + function createDiagonalPattern(backgroundColor, lineColor, rightToLeft=false, lineGap=1) { + // Define the canvas and the 2d context so we can draw on it + let shape = document.createElement("canvas"); + shape.width = 20; + shape.height = 20; + let context = shape.getContext("2d"); - function createDiagonalRightLeftPattern(backgroundColor, lineColor="white") { - // create a 20x20 px canvas for larger pattern repeat - let shape = document.createElement("canvas") - shape.width = 20 - shape.height = 20 - // get the context for drawing - let c = shape.getContext("2d") - // Fill with specified background color - c.fillStyle = backgroundColor - c.fillRect(0, 0, shape.width, shape.height) - - // Translate and rotate context - c.translate(shape.width, 0) - c.rotate(90 * Math.PI / 180) - + context.fillStyle = backgroundColor; + context.fillRect(0, 0, shape.width, shape.height); + // Set stroke properties - c.strokeStyle = lineColor - c.lineWidth = 2 - + context.strokeStyle = lineColor; + context.lineWidth = 2; + + // Rotate canvas for a right-to-left pattern + if (rightToLeft) { + context.translate(shape.width, 0); + context.rotate(90 * Math.PI / 180); + }; + // First diagonal line - let halfSize = shape.width / 2 - let gap = 1 - - c.moveTo(halfSize - gap, -gap) - c.lineTo(shape.width + 1, halfSize + 1) - - // Second diagonal line (offset) - c.moveTo(halfSize - gap - halfSize, halfSize - gap) - c.lineTo(shape.width + 1 - halfSize, halfSize + 1 + halfSize) - - c.stroke() - - return c.createPattern(shape, "repeat") + let halfSize = shape.width / 2; + context.moveTo(halfSize - lineGap, -lineGap); + context.lineTo(shape.width + lineGap, halfSize + lineGap); + + // Second diagonal line (x,y are swapped) + context.moveTo(-lineGap, halfSize - lineGap); + context.lineTo(halfSize + lineGap, shape.width + lineGap); + + context.stroke(); + return context.createPattern(shape, "repeat"); } function createComparativeColumnChart(canvasId, title, labelOne, labelTwo) { @@ -155,7 +124,8 @@ borderColor: "rgba(255, 99, 132, 1)", borderWidth: 1, data: listOne, - backgroundColor: createDiagonalRightLeftPattern('rgba(255, 99, 132, 0.3)') + // Set this line style to be rightToLeft for visual distinction + backgroundColor: createDiagonalPattern('rgba(255, 99, 132, 0.3)', 'white', true) }, { label: labelTwo, @@ -163,7 +133,7 @@ borderColor: "rgba(75, 192, 192, 1)", borderWidth: 1, data: listTwo, - backgroundColor: createDiagonalPattern('rgba(75, 192, 192, 0.3)') + backgroundColor: createDiagonalPattern('rgba(75, 192, 192, 0.3)', 'white') }, ], }; From 69596fe5bbd82fd3efbe8a82706033941cfbd82f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:19:12 -0700 Subject: [PATCH 106/139] Update src/registrar/assets/js/get-gov-reports.js --- src/registrar/assets/js/get-gov-reports.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index 382e91fc6..b82a5574f 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -64,8 +64,8 @@ * Creates a diagonal stripe pattern for chart.js * Inspired by https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns * and https://github.com/ashiguruma/patternomaly - * @param {string} [lineColor="white"] - Color of the diagonal lines * @param {string} backgroundColor - Background color of the pattern + * @param {string} [lineColor="white"] - Color of the diagonal lines * @param {boolean} [rightToLeft=false] - Direction of the diagonal lines * @param {number} [lineGap=1] - Gap between lines * @returns {CanvasPattern} A canvas pattern object for use with backgroundColor From 4e95c77725a716d375cc6d097a56e35c4993ed8a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:54:12 -0700 Subject: [PATCH 107/139] cleanup html --- src/registrar/templates/admin/analytics.html | 479 +++++++++---------- 1 file changed, 239 insertions(+), 240 deletions(-) diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index 91248f502..ca3501eec 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -1,6 +1,7 @@ {% extends "admin/base_site.html" %} {% load static %} {% load i18n %} + {% block content_title %}

    Registrar Analytics

    {% endblock %} {% block breadcrumbs %} @@ -14,251 +15,249 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/ {% trans "Analytics Dashboard" %}
{% endblock %} + {% block content %} -
-
-
-
-

At a glance

-
-
    -
  • User Count: {{ data.user_count }}
  • -
  • Domain Count: {{ data.domain_count }}
  • -
  • Domains in READY state: {{ data.ready_domain_count }}
  • -
  • Domain applications (last 30 days): {{ data.last_30_days_applications }}
  • -
  • Approved applications (last 30 days): {{ data.last_30_days_approved_applications }}
  • -
  • Average approval time for applications (last 30 days): {{ data.average_application_approval_time_last_30_days }}
  • -
-
-
-
- -
-
-
-
-

Growth reports

-
- {% comment %} - Inputs of type date suck for accessibility. - We'll need to replace those guys with a django form once we figure out how to hook one onto this page. - See the commit "Review for ticket #999" - {% endcomment %} -
-
- - -
-
- - -
-
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
- {% comment %} Managed/Unmanaged domains {% endcomment %} -
- -

Chart: Managed domains

-

{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

-
-
-
-
- Details for managed domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %} -
-
-
-
- -

Chart: Unmanaged domains

-

{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

-
-
-
-
- Details for unmanaged domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %} -
-
-
- {% comment %} Deleted/Ready domains {% endcomment %} -
- -

Chart: Deleted domains

-

{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

-
-
-
-
- Details for deleted domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %} -
-
-
-
- -

Chart: Ready domains

-

{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

-
-
-
-
- Details for ready domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %} -
-
-
+
- {% comment %} Requests {% endcomment %} -
- -

Chart: Submitted requests

-

{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

-
-
-
-
- Details for submitted requests -
- {% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %} -
-
-
-
- -

Chart: All requests

-

{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

-
-
-
-
- Details for all requests -
- {% include "admin/analytics_graph_table.html" with data=data property_name="requests" %} -
-
+
+
+
+

At a glance

+
+
    +
  • User Count: {{ data.user_count }}
  • +
  • Domain Count: {{ data.domain_count }}
  • +
  • Domains in READY state: {{ data.ready_domain_count }}
  • +
  • Domain applications (last 30 days): {{ data.last_30_days_applications }}
  • +
  • Approved applications (last 30 days): {{ data.last_30_days_approved_applications }}
  • +
  • Average approval time for applications (last 30 days): {{ data.average_application_approval_time_last_30_days }}
  • +
+
+ +
+ +
+
+
+

Growth reports

+
+ {% comment %} + Inputs of type date suck for accessibility. + We'll need to replace those guys with a django form once we figure out how to hook one onto this page. + See the commit "Review for ticket #999" + {% endcomment %} +
+
+ + +
+
+ + +
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ +
+ {% comment %} Managed/Unmanaged domains {% endcomment %} +
+ +

Chart: Managed domains

+

{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

+
+
+
+
+ Details for managed domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %} +
+
+
+
+ +

Chart: Unmanaged domains

+

{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

+
+
+
+
+ Details for unmanaged domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %} +
+
+
+ + {% comment %} Deleted/Ready domains {% endcomment %} +
+ +

Chart: Deleted domains

+

{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

+
+
+
+
+ Details for deleted domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %} +
+
+
+
+ +

Chart: Ready domains

+

{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

+
+
+
+
+ Details for ready domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %} +
+
+
+ + {% comment %} Requests {% endcomment %} +
+ +

Chart: Submitted requests

+

{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

+
+
+
+
+ Details for submitted requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %} +
+
+
+
+ +

Chart: All requests

+

{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

+
+
+
+
+ Details for all requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="requests" %} +
+
-
+ +
+
+
+
-{% endblock %} \ No newline at end of file +{% endblock %} From bb05f6f4657c1d6aaab06e3658b128ee86f744a8 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 11 Feb 2025 12:50:40 -0800 Subject: [PATCH 108/139] Add logic for getting envs and adding it to email subject and body --- src/registrar/config/settings.py | 1 + .../templates/emails/domain_invitation.txt | 2 +- .../emails/domain_invitation_subject.txt | 2 +- ...n_manager_deleted_notification_subject.txt | 2 +- .../emails/domain_manager_notification.txt | 2 +- .../domain_manager_notification_subject.txt | 2 +- .../emails/domain_request_withdrawn.txt | 2 +- .../domain_request_withdrawn_subject.txt | 2 +- .../templates/emails/metadata_body.txt | 2 +- .../templates/emails/metadata_subject.txt | 2 +- .../portfolio_admin_addition_notification.txt | 2 +- ...io_admin_addition_notification_subject.txt | 2 +- .../portfolio_admin_removal_notification.txt | 2 +- ...lio_admin_removal_notification_subject.txt | 2 +- .../templates/emails/portfolio_invitation.txt | 2 +- .../emails/portfolio_invitation_subject.txt | 2 +- .../emails/status_change_approved.txt | 2 +- .../emails/status_change_approved_subject.txt | 2 +- .../emails/status_change_subject.txt | 2 +- .../emails/submission_confirmation.txt | 2 +- .../submission_confirmation_subject.txt | 2 +- .../emails/transition_domain_invitation.txt | 2 +- .../transition_domain_invitation_subject.txt | 2 +- .../emails/update_to_approved_domain.txt | 4 ++-- .../update_to_approved_domain_subject.txt | 2 +- src/registrar/utility/email.py | 19 +++++++++++++++++++ 26 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 78439188e..fa4c2d8dc 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -107,6 +107,7 @@ DEBUG = env_debug # Controls production specific feature toggles IS_PRODUCTION = env_is_production SECRET_ENCRYPT_METADATA = secret_encrypt_metadata +BASE_URL = env_base_url # Applications are modular pieces of code. # They are provided by Django, by third-parties, or by yourself. diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index a077bff26..270786a7a 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -4,7 +4,7 @@ Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first {{ requestor_email }} has invited you to manage: {% for domain in domains %}{{ domain.name }} {% endfor %} -To manage domain information, visit the .gov registrar . +To manage domain information, visit the .gov registrar <{{ manage_url }}>. ---------------------------------------------------------------- {% if not requested_user %} diff --git a/src/registrar/templates/emails/domain_invitation_subject.txt b/src/registrar/templates/emails/domain_invitation_subject.txt index 9663346d0..9f15c38b4 100644 --- a/src/registrar/templates/emails/domain_invitation_subject.txt +++ b/src/registrar/templates/emails/domain_invitation_subject.txt @@ -1 +1 @@ -You've been invited to manage {% if domains|length > 1 %}.gov domains{% else %}{{ domains.0.name }}{% endif %} \ No newline at end of file +{{ prefix }}You've been invited to manage {% if domains|length > 1 %}.gov domains{% else %}{{ domains.0.name }}{% endif %} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt b/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt index c84a20f18..7376bdb86 100644 --- a/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt +++ b/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt @@ -1 +1 @@ -A domain manager was removed from {{ domain.name }} \ No newline at end of file +{{ prefix }}A domain manager was removed from {{ domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_manager_notification.txt b/src/registrar/templates/emails/domain_manager_notification.txt index c253937e4..18e682329 100644 --- a/src/registrar/templates/emails/domain_manager_notification.txt +++ b/src/registrar/templates/emails/domain_manager_notification.txt @@ -15,7 +15,7 @@ The person who received the invitation will become a domain manager once they lo associated with the invited email address. If you need to cancel this invitation or remove the domain manager, you can do that by going to -this domain in the .gov registrar . +this domain in the .gov registrar <{{ manage_url }}. WHY DID YOU RECEIVE THIS EMAIL? diff --git a/src/registrar/templates/emails/domain_manager_notification_subject.txt b/src/registrar/templates/emails/domain_manager_notification_subject.txt index 0e9918de0..8560cb9fa 100644 --- a/src/registrar/templates/emails/domain_manager_notification_subject.txt +++ b/src/registrar/templates/emails/domain_manager_notification_subject.txt @@ -1 +1 @@ -A domain manager was invited to {{ domain.name }} \ No newline at end of file +{{ prefix }}A domain manager was invited to {{ domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_request_withdrawn.txt b/src/registrar/templates/emails/domain_request_withdrawn.txt index fbdf5b4f1..fe026027b 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn.txt @@ -11,7 +11,7 @@ STATUS: Withdrawn ---------------------------------------------------------------- YOU CAN EDIT YOUR WITHDRAWN REQUEST -You can edit and resubmit this request by signing in to the registrar . +You can edit and resubmit this request by signing in to the registrar <{{ manage_url }}>. SOMETHING WRONG? diff --git a/src/registrar/templates/emails/domain_request_withdrawn_subject.txt b/src/registrar/templates/emails/domain_request_withdrawn_subject.txt index 51b2c745a..cc146643a 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn_subject.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/metadata_body.txt b/src/registrar/templates/emails/metadata_body.txt index adf0a186c..a0a3682b7 100644 --- a/src/registrar/templates/emails/metadata_body.txt +++ b/src/registrar/templates/emails/metadata_body.txt @@ -1 +1 @@ -An export of all .gov metadata. +{{ prefix }}An export of all .gov metadata. diff --git a/src/registrar/templates/emails/metadata_subject.txt b/src/registrar/templates/emails/metadata_subject.txt index 5fdece7ef..c19b4c26e 100644 --- a/src/registrar/templates/emails/metadata_subject.txt +++ b/src/registrar/templates/emails/metadata_subject.txt @@ -1,2 +1,2 @@ -Domain metadata - {{current_date_str}} +{{ prefix }}Domain metadata - {{current_date_str}} diff --git a/src/registrar/templates/emails/portfolio_admin_addition_notification.txt b/src/registrar/templates/emails/portfolio_admin_addition_notification.txt index b8953aa67..9e6da3985 100644 --- a/src/registrar/templates/emails/portfolio_admin_addition_notification.txt +++ b/src/registrar/templates/emails/portfolio_admin_addition_notification.txt @@ -16,7 +16,7 @@ The person who received the invitation will become an admin once they log in to associated with the invited email address. If you need to cancel this invitation or remove the admin, you can do that by going to -the Members section for your organization . +the Members section for your organization <{{ manage_url }}>. WHY DID YOU RECEIVE THIS EMAIL? diff --git a/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt b/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt index 3d6b2a140..ee5987512 100644 --- a/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt +++ b/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt @@ -1 +1 @@ -An admin was invited to your .gov organization \ No newline at end of file +{{ prefix }}An admin was invited to your .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/portfolio_admin_removal_notification.txt b/src/registrar/templates/emails/portfolio_admin_removal_notification.txt index 6a536aa49..bf0338c03 100644 --- a/src/registrar/templates/emails/portfolio_admin_removal_notification.txt +++ b/src/registrar/templates/emails/portfolio_admin_removal_notification.txt @@ -8,7 +8,7 @@ REMOVED BY: {{ requestor_email }} REMOVED ON: {{date}} ADMIN REMOVED: {{ removed_email_address }} -You can view this update by going to the Members section for your .gov organization . +You can view this update by going to the Members section for your .gov organization <{{ manage_url }}>. ---------------------------------------------------------------- diff --git a/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt index e250b17f8..030d27ae7 100644 --- a/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt +++ b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt @@ -1 +1 @@ -An admin was removed from your .gov organization \ No newline at end of file +{{ prefix}}An admin was removed from your .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/portfolio_invitation.txt b/src/registrar/templates/emails/portfolio_invitation.txt index 775b74c7c..893da153d 100644 --- a/src/registrar/templates/emails/portfolio_invitation.txt +++ b/src/registrar/templates/emails/portfolio_invitation.txt @@ -3,7 +3,7 @@ Hi. {{ requestor_email }} has invited you to {{ portfolio.organization_name }}. -You can view this organization on the .gov registrar . +You can view this organization on the .gov registrar <{{ manage_url }}>. ---------------------------------------------------------------- diff --git a/src/registrar/templates/emails/portfolio_invitation_subject.txt b/src/registrar/templates/emails/portfolio_invitation_subject.txt index 552bb2bec..de9080196 100644 --- a/src/registrar/templates/emails/portfolio_invitation_subject.txt +++ b/src/registrar/templates/emails/portfolio_invitation_subject.txt @@ -1 +1 @@ -You’ve been invited to a .gov organization \ No newline at end of file +{{ prefix }}You’ve been invited to a .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/status_change_approved.txt b/src/registrar/templates/emails/status_change_approved.txt index 821e89e42..635b36cbd 100644 --- a/src/registrar/templates/emails/status_change_approved.txt +++ b/src/registrar/templates/emails/status_change_approved.txt @@ -8,7 +8,7 @@ REQUESTED BY: {{ domain_request.creator.email }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} STATUS: Approved -You can manage your approved domain on the .gov registrar . +You can manage your approved domain on the .gov registrar <{{ manage_url }}>. ---------------------------------------------------------------- diff --git a/src/registrar/templates/emails/status_change_approved_subject.txt b/src/registrar/templates/emails/status_change_approved_subject.txt index 51b2c745a..cc146643a 100644 --- a/src/registrar/templates/emails/status_change_approved_subject.txt +++ b/src/registrar/templates/emails/status_change_approved_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/status_change_subject.txt b/src/registrar/templates/emails/status_change_subject.txt index 51b2c745a..cc146643a 100644 --- a/src/registrar/templates/emails/status_change_subject.txt +++ b/src/registrar/templates/emails/status_change_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index d9d01ec3e..afbde48d5 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -20,7 +20,7 @@ During our review, we’ll verify that: - You work at the organization and/or can make requests on its behalf - Your requested domain meets our naming requirements {% endif %} -We’ll email you if we have questions. We’ll also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. . +We’ll email you if we have questions. We’ll also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. <{{ manage_url }}>. NEED TO MAKE CHANGES? diff --git a/src/registrar/templates/emails/submission_confirmation_subject.txt b/src/registrar/templates/emails/submission_confirmation_subject.txt index 51b2c745a..cc146643a 100644 --- a/src/registrar/templates/emails/submission_confirmation_subject.txt +++ b/src/registrar/templates/emails/submission_confirmation_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/transition_domain_invitation.txt b/src/registrar/templates/emails/transition_domain_invitation.txt index b6773d9e9..dc812edf3 100644 --- a/src/registrar/templates/emails/transition_domain_invitation.txt +++ b/src/registrar/templates/emails/transition_domain_invitation.txt @@ -31,7 +31,7 @@ CHECK YOUR .GOV DOMAIN CONTACTS This is a good time to check who has access to your .gov domain{% if domains|length > 1 %}s{% endif %}. The admin, technical, and billing contacts listed for your domain{% if domains|length > 1 %}s{% endif %} in our old system also received this email. In our new registrar, these contacts are all considered “domain managers.” We no longer have the admin, technical, and billing roles, and you aren’t limited to three domain managers like in the old system. - 1. Once you have your Login.gov account, sign in to the new registrar at . + 1. Once you have your Login.gov account, sign in to the new registrar at <{{ manage_url }}>. 2. Click the “Manage” link next to your .gov domain, then click on “Domain managers” to see who has access to your domain. 3. If any of these users should not have access to your domain, let us know in a reply to this email. diff --git a/src/registrar/templates/emails/transition_domain_invitation_subject.txt b/src/registrar/templates/emails/transition_domain_invitation_subject.txt index 526c7714b..b162341d9 100644 --- a/src/registrar/templates/emails/transition_domain_invitation_subject.txt +++ b/src/registrar/templates/emails/transition_domain_invitation_subject.txt @@ -1 +1 @@ -(Action required) Manage your .gov domain{% if domains|length > 1 %}s{% endif %} in the new registrar \ No newline at end of file +{{ prefix }}(Action required) Manage your .gov domain{% if domains|length > 1 %}s{% endif %} in the new registrar \ No newline at end of file diff --git a/src/registrar/templates/emails/update_to_approved_domain.txt b/src/registrar/templates/emails/update_to_approved_domain.txt index 99f86ea54..fb0a442cb 100644 --- a/src/registrar/templates/emails/update_to_approved_domain.txt +++ b/src/registrar/templates/emails/update_to_approved_domain.txt @@ -1,4 +1,4 @@ -{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} + {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} Hi, An update was made to a domain you manage. @@ -8,7 +8,7 @@ UPDATED BY: {{user}} UPDATED ON: {{date}} INFORMATION UPDATED: {{changes}} -You can view this update in the .gov registrar . +You can view this update in the .gov registrar <{{ manage_url }}>. Get help with managing your .gov domain . diff --git a/src/registrar/templates/emails/update_to_approved_domain_subject.txt b/src/registrar/templates/emails/update_to_approved_domain_subject.txt index cf4c9a14c..d952999a0 100644 --- a/src/registrar/templates/emails/update_to_approved_domain_subject.txt +++ b/src/registrar/templates/emails/update_to_approved_domain_subject.txt @@ -1 +1 @@ -An update was made to {{domain}} \ No newline at end of file +{{ prefix }}An update was made to {{domain}} \ No newline at end of file diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 40601cdc7..535096b10 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -3,6 +3,7 @@ import boto3 import logging import textwrap +import re from datetime import datetime from django.apps import apps from django.conf import settings @@ -48,6 +49,24 @@ def send_templated_email( # noqa No valid recipient addresses are provided """ + if context is None: + context = {} + + env_base_url = settings.BASE_URL + # The regular expresstion is to get both http (localhost) and https (everything else) + env_name = re.sub(r"^https?://", "", env_base_url).split(".")[0] + # To add to subject lines ie [GETGOV-RH] + prefix = f"[{env_name.upper()}] " if not settings.IS_PRODUCTION else "" + # For email links + manage_url = env_base_url if not settings.IS_PRODUCTION else "https://manage.get.gov" + + # Adding to context + context.update( + { + "prefix": prefix, + "manage_url": manage_url, + } + ) # by default assume we can send to all addresses (prod has no whitelist) sendable_cc_addresses = cc_addresses From 0fc19aa741cdd306bd676b09ee183a9aa62de99c Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 11 Feb 2025 14:11:31 -0800 Subject: [PATCH 109/139] Fix SCSS to allow hover and cursor --- src/registrar/assets/src/sass/_theme/_admin.scss | 6 ++++++ .../templates/admin/change_form_object_tools.html | 7 ++----- .../django/admin/includes/email_clipboard_fieldset.html | 2 +- src/registrar/tests/test_admin_domain.py | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index 64b979f2b..322e94bf0 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -512,6 +512,12 @@ input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-too text-transform: uppercase; letter-spacing: 0.5px; border-radius: 15px; + cursor: pointer; + border: none; + line-height: 20px; + &:focus, &:hover{ + background: var(--object-tools-hover-bg) !important; + } } .module--custom { diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index a76609538..d76d899a8 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -22,10 +22,7 @@ {% csrf_token %} @@ -50,4 +47,4 @@ {% endif %} {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html b/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html index 733fc3787..f959f8edf 100644 --- a/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html +++ b/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html @@ -10,4 +10,4 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% else %} {{ block.super }} {% endif %} -{% endblock field_other %} \ No newline at end of file +{% endblock field_other %} diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py index 24ade9302..4cae9a9e0 100644 --- a/src/registrar/tests/test_admin_domain.py +++ b/src/registrar/tests/test_admin_domain.py @@ -496,7 +496,7 @@ class TestDomainInformationInline(MockEppLib): self.assertIn("poopy@gov.gov", domain_managers) -class DomainInvitationAdminTest(TestCase): +class TestDomainInvitationAdmin(TestCase): @classmethod def setUpClass(cls): From a87fdc6e1c837c5ae3d9c597bd98850776d5a605 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 11 Feb 2025 14:14:26 -0800 Subject: [PATCH 110/139] Fix spacing --- src/registrar/templates/admin/change_form_object_tools.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index d76d899a8..2f3d282ea 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -28,7 +28,6 @@ {% endif %} {% endif %} -
  • {% translate "History" %} From 898a66ccc90fcb1058afe5e493da36e43736cfe7 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Wed, 12 Feb 2025 09:44:39 -0500 Subject: [PATCH 111/139] removed domain renewal feature --- src/registrar/context_processors.py | 3 -- src/registrar/models/domain.py | 2 +- src/registrar/models/user.py | 3 -- src/registrar/templates/domain_detail.html | 10 ++--- src/registrar/templates/domain_sidebar.html | 2 +- .../templates/includes/domains_table.html | 6 +-- src/registrar/tests/test_views_domain.py | 39 +++---------------- src/registrar/views/domain.py | 3 +- 8 files changed, 16 insertions(+), 52 deletions(-) diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index a078c81ac..061c0ab4f 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -68,7 +68,6 @@ def portfolio_permissions(request): "has_organization_requests_flag": False, "has_organization_members_flag": False, "is_portfolio_admin": False, - "has_domain_renewal_flag": False, } try: portfolio = request.session.get("portfolio") @@ -77,7 +76,6 @@ def portfolio_permissions(request): portfolio_context.update( { "has_organization_feature_flag": True, - "has_domain_renewal_flag": request.user.has_domain_renewal_flag(), } ) @@ -95,7 +93,6 @@ def portfolio_permissions(request): "has_organization_requests_flag": request.user.has_organization_requests_flag(), "has_organization_members_flag": request.user.has_organization_members_flag(), "is_portfolio_admin": request.user.is_portfolio_admin(portfolio), - "has_domain_renewal_flag": request.user.has_domain_renewal_flag(), } return portfolio_context diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 0f0b3f112..cd768f76c 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1583,7 +1583,7 @@ class Domain(TimeStampedModel, DomainHelper): # Given expired is not a physical state, but it is displayed as such, # We need custom logic to determine this message. help_text = "This domain has expired. Complete the online renewal process to maintain access." - elif flag_is_active(request, "domain_renewal") and self.is_expiring(): + elif self.is_expiring(): help_text = "This domain is expiring soon. Complete the online renewal process to maintain access." else: help_text = Domain.State.get_help_text(self.state) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 6f8ee499b..82a0465c5 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -271,9 +271,6 @@ class User(AbstractUser): def is_portfolio_admin(self, portfolio): return "Admin" in self.portfolio_role_summary(portfolio) - def has_domain_renewal_flag(self): - return flag_is_active_for_user(self, "domain_renewal") - def get_first_portfolio(self): permission = self.portfolio_permissions.first() if permission: diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 758c43366..57749f038 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -35,7 +35,7 @@ {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #} {% if domain.is_expired and domain.state != domain.State.UNKNOWN %} Expired - {% elif has_domain_renewal_flag and domain.is_expiring %} + {% elif domain.is_expiring %} Expiring soon {% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %} DNS needed @@ -46,17 +46,17 @@ {% if domain.get_state_help_text %}

    - {% if has_domain_renewal_flag and domain.is_expired and is_domain_manager %} + {% if domain.is_expired and is_domain_manager %} This domain has expired, but it is still online. {% url 'domain-renewal' pk=domain.id as url %} Renew to maintain access. - {% elif has_domain_renewal_flag and domain.is_expiring and is_domain_manager %} + {% elif domain.is_expiring and is_domain_manager %} This domain will expire soon. {% url 'domain-renewal' pk=domain.id as url %} Renew to maintain access. - {% elif has_domain_renewal_flag and domain.is_expiring and is_portfolio_user %} + {% elif domain.is_expiring and is_portfolio_user %} This domain will expire soon. Contact one of the listed domain managers to renew the domain. - {% elif has_domain_renewal_flag and domain.is_expired and is_portfolio_user %} + {% elif domain.is_expired and is_portfolio_user %} This domain has expired, but it is still online. Contact one of the listed domain managers to renew the domain. {% else %} {{ domain.get_state_help_text }} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 5946b6859..3302a6a79 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -81,7 +81,7 @@ {% endwith %} - {% if has_domain_renewal_flag and is_domain_manager%} + {% if is_domain_manager%} {% if domain.is_expiring or domain.is_expired %} {% with url_name="domain-renewal" %} {% include "includes/domain_sidenav_item.html" with item_text="Renewal form" %} diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html index 94cb4ea6d..3cf04a830 100644 --- a/src/registrar/templates/includes/domains_table.html +++ b/src/registrar/templates/includes/domains_table.html @@ -9,7 +9,7 @@ -{% if has_domain_renewal_flag and num_expiring_domains > 0 and has_any_domains_portfolio_permission %} +{% if num_expiring_domains > 0 and has_any_domains_portfolio_permission %}

    @@ -75,7 +75,7 @@
    - {% if has_domain_renewal_flag and num_expiring_domains > 0 and not portfolio %} + {% if num_expiring_domains > 0 and not portfolio %}
    @@ -173,7 +173,6 @@ >Deleted
    - {% if has_domain_renewal_flag %}
    Expiring soon
    - {% endif %}
    diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index dc5bff27a..2f1bcf5e3 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -477,7 +477,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.domain_with_ip.expiration_date = self.expiration_date_one_year_out() self.domain_with_ip.save() - @override_flag("domain_renewal", active=True) def test_expiring_domain_on_detail_page_as_domain_manager(self): """If a user is a domain manager and their domain is expiring soon, user should be able to see the "Renew to maintain access" link domain overview detail box.""" @@ -496,7 +495,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertNotContains(detail_page, "DNS needed") self.assertNotContains(detail_page, "Expired") - @override_flag("domain_renewal", active=True) @override_flag("organization_feature", active=True) def test_expiring_domain_on_detail_page_in_org_model_as_a_non_domain_manager(self): """In org model: If a user is NOT a domain manager and their domain is expiring soon, @@ -534,7 +532,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): ) self.assertContains(detail_page, "Contact one of the listed domain managers to renew the domain.") - @override_flag("domain_renewal", active=True) @override_flag("organization_feature", active=True) def test_expiring_domain_on_detail_page_in_org_model_as_a_domain_manager(self): """Inorg model: If a user is a domain manager and their domain is expiring soon, @@ -555,7 +552,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): ) self.assertContains(detail_page, "Renew to maintain access") - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_and_sidebar_expiring(self): """If a user is a domain manager and their domain is expiring soon, user should be able to see Renewal Form on the sidebar.""" @@ -584,7 +580,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertEqual(response.status_code, 200) self.assertContains(response, f"Renew {self.domain_to_renew.name}") - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_and_sidebar_expired(self): """If a user is a domain manager and their domain is expired, user should be able to see Renewal Form on the sidebar.""" @@ -614,7 +609,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertEqual(response.status_code, 200) self.assertContains(response, f"Renew {self.domain_to_renew.name}") - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_your_contact_info_edit(self): """Checking that if a user is a domain manager they can edit the Your Profile portion of the Renewal Form.""" @@ -634,7 +628,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertEqual(edit_page.status_code, 200) self.assertContains(edit_page, "Review the details below and update any required information") - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_security_email_edit(self): """Checking that if a user is a domain manager they can edit the Security Email portion of the Renewal Form.""" @@ -657,7 +650,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertEqual(edit_page.status_code, 200) self.assertContains(edit_page, "A security contact should be capable of evaluating") - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_domain_manager_edit(self): """Checking that if a user is a domain manager they can edit the Domain Manager portion of the Renewal Form.""" @@ -677,7 +669,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertEqual(edit_page.status_code, 200) self.assertContains(edit_page, "Domain managers can update all information related to a domain") - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_not_expired_or_expiring(self): """Checking that if the user's domain is not expired or expiring that user should not be able to access /renewal and that it should receive a 403.""" @@ -686,7 +677,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domain_not_expiring.id})) self.assertEqual(renewal_page.status_code, 403) - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_does_not_appear_if_not_domain_manager(self): """If user is not a domain manager and tries to access /renewal, user should receive a 403.""" with patch.object(Domain, "is_expired", self.custom_is_expired_true), patch.object( @@ -695,7 +685,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domain_no_domain_manager.id})) self.assertEqual(renewal_page.status_code, 403) - @override_flag("domain_renewal", active=True) def test_ack_checkbox_not_checked(self): """If user don't check the checkbox, user should receive an error message.""" # Grab the renewal URL @@ -707,7 +696,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): error_message = "Check the box if you read and agree to the requirements for operating a .gov domain." self.assertContains(response, error_message) - @override_flag("domain_renewal", active=True) def test_ack_checkbox_checked(self): """If user check the checkbox and submits the form, user should be redirected Domain Over page with an updated by 1 year expiration date""" @@ -2966,26 +2954,15 @@ class TestDomainRenewal(TestWithUser): pass super().tearDown() - # Remove test_without_domain_renewal_flag when domain renewal is released as a feature @less_console_noise_decorator - @override_flag("domain_renewal", active=False) - def test_without_domain_renewal_flag(self): - self.client.force_login(self.user) - domains_page = self.client.get("/") - self.assertNotContains(domains_page, "will expire soon") - self.assertNotContains(domains_page, "Expiring soon") - - @less_console_noise_decorator - @override_flag("domain_renewal", active=True) - def test_domain_renewal_flag_single_domain(self): + def test_domain_with_single_domain(self): self.client.force_login(self.user) domains_page = self.client.get("/") self.assertContains(domains_page, "One domain will expire soon") self.assertContains(domains_page, "Expiring soon") @less_console_noise_decorator - @override_flag("domain_renewal", active=True) - def test_with_domain_renewal_flag_mulitple_domains(self): + def test_with_mulitple_domains(self): today = datetime.now() expiring_date = (today + timedelta(days=30)).strftime("%Y-%m-%d") self.domain_with_another_expiring, _ = Domain.objects.get_or_create( @@ -3001,8 +2978,7 @@ class TestDomainRenewal(TestWithUser): self.assertContains(domains_page, "Expiring soon") @less_console_noise_decorator - @override_flag("domain_renewal", active=True) - def test_with_domain_renewal_flag_no_expiring_domains(self): + def test_with_no_expiring_domains(self): UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expired_date).delete() UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expiring_soon_date).delete() self.client.force_login(self.user) @@ -3010,18 +2986,16 @@ class TestDomainRenewal(TestWithUser): self.assertNotContains(domains_page, "will expire soon") @less_console_noise_decorator - @override_flag("domain_renewal", active=True) @override_flag("organization_feature", active=True) - def test_domain_renewal_flag_single_domain_w_org_feature_flag(self): + def test_single_domain_w_org_feature_flag(self): self.client.force_login(self.user) domains_page = self.client.get("/") self.assertContains(domains_page, "One domain will expire soon") self.assertContains(domains_page, "Expiring soon") @less_console_noise_decorator - @override_flag("domain_renewal", active=True) @override_flag("organization_feature", active=True) - def test_with_domain_renewal_flag_mulitple_domains_w_org_feature_flag(self): + def test_with_mulitple_domains_w_org_feature_flag(self): today = datetime.now() expiring_date = (today + timedelta(days=31)).strftime("%Y-%m-%d") self.domain_with_another_expiring_org_model, _ = Domain.objects.get_or_create( @@ -3037,9 +3011,8 @@ class TestDomainRenewal(TestWithUser): self.assertContains(domains_page, "Expiring soon") @less_console_noise_decorator - @override_flag("domain_renewal", active=True) @override_flag("organization_feature", active=True) - def test_with_domain_renewal_flag_no_expiring_domains_w_org_feature_flag(self): + def test_no_expiring_domains_w_org_feature_flag(self): UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expired_date).delete() UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expiring_soon_date).delete() self.client.force_login(self.user) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 297cb689a..27ee44068 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -366,7 +366,7 @@ class DomainRenewalView(DomainBaseView): return HttpResponseRedirect(reverse("domain", kwargs={"pk": pk})) # if not valid, render the template with error messages - # passing editable, has_domain_renewal_flag, and is_editable for re-render + # passing editable,å and is_editable for re-render return render( request, "domain_renewal.html", @@ -374,7 +374,6 @@ class DomainRenewalView(DomainBaseView): "domain": domain, "form": form, "is_editable": True, - "has_domain_renewal_flag": True, "is_domain_manager": True, }, ) From 2a6d401e9b23c2c8d22bedf8282903b55f3ea6ee Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:00:32 -0700 Subject: [PATCH 112/139] Move get-gov-reports to src/ @rachidatecs see above ^ --- src/registrar/assets/js/get-gov-reports.js | 203 ------------------ .../assets/src/js/getgov-admin/analytics.js | 196 +++++++++++++++++ .../src/js/getgov-admin/helpers-admin.js | 10 + .../assets/src/js/getgov-admin/main.js | 4 + src/registrar/templates/admin/analytics.html | 14 +- src/registrar/templates/admin/base_site.html | 1 - 6 files changed, 217 insertions(+), 211 deletions(-) delete mode 100644 src/registrar/assets/js/get-gov-reports.js create mode 100644 src/registrar/assets/src/js/getgov-admin/analytics.js diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js deleted file mode 100644 index 54cdc789a..000000000 --- a/src/registrar/assets/js/get-gov-reports.js +++ /dev/null @@ -1,203 +0,0 @@ - -/** An IIFE for admin in DjangoAdmin to listen to clicks on the growth report export button, - * attach the seleted start and end dates to a url that'll trigger the view, and finally - * redirect to that url. - * - * This function also sets the start and end dates to match the url params if they exist -*/ -(function () { - // Function to get URL parameter value by name - function getParameterByName(name, url) { - if (!url) url = window.location.href; - name = name.replace(/[\[\]]/g, '\\$&'); - var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), - results = regex.exec(url); - if (!results) return null; - if (!results[2]) return ''; - return decodeURIComponent(results[2].replace(/\+/g, ' ')); - } - - // Get the current date in the format YYYY-MM-DD - let currentDate = new Date().toISOString().split('T')[0]; - - // Default the value of the start date input field to the current date - let startDateInput = document.getElementById('start'); - - // Default the value of the end date input field to the current date - let endDateInput = document.getElementById('end'); - - let exportButtons = document.querySelectorAll('.exportLink'); - - if (exportButtons.length > 0) { - // Check if start and end dates are present in the URL - let urlStartDate = getParameterByName('start_date'); - let urlEndDate = getParameterByName('end_date'); - - // Set input values based on URL parameters or current date - startDateInput.value = urlStartDate || currentDate; - endDateInput.value = urlEndDate || currentDate; - - exportButtons.forEach((btn) => { - btn.addEventListener('click', function () { - // Get the selected start and end dates - let startDate = startDateInput.value; - let endDate = endDateInput.value; - let exportUrl = btn.dataset.exportUrl; - - // Build the URL with parameters - exportUrl += "?start_date=" + startDate + "&end_date=" + endDate; - - // Redirect to the export URL - window.location.href = exportUrl; - }); - }); - } - -})(); - - -/** An IIFE to initialize the analytics page -*/ -(function () { - const chartInstances = new Map(); - - /** - * Creates a diagonal stripe pattern for chart.js - * Inspired by https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns - * and https://github.com/ashiguruma/patternomaly - * @param {string} backgroundColor - Background color of the pattern - * @param {string} [lineColor="white"] - Color of the diagonal lines - * @param {boolean} [rightToLeft=false] - Direction of the diagonal lines - * @param {number} [lineGap=1] - Gap between lines - * @returns {CanvasPattern} A canvas pattern object for use with backgroundColor - */ - function createDiagonalPattern(backgroundColor, lineColor, rightToLeft=false, lineGap=1) { - // Define the canvas and the 2d context so we can draw on it - let shape = document.createElement("canvas"); - shape.width = 20; - shape.height = 20; - let context = shape.getContext("2d"); - - // Fill with specified background color - context.fillStyle = backgroundColor; - context.fillRect(0, 0, shape.width, shape.height); - - // Set stroke properties - context.strokeStyle = lineColor; - context.lineWidth = 2; - - // Rotate canvas for a right-to-left pattern - if (rightToLeft) { - context.translate(shape.width, 0); - context.rotate(90 * Math.PI / 180); - }; - - // First diagonal line - let halfSize = shape.width / 2; - context.moveTo(halfSize - lineGap, -lineGap); - context.lineTo(shape.width + lineGap, halfSize + lineGap); - - // Second diagonal line (x,y are swapped) - context.moveTo(-lineGap, halfSize - lineGap); - context.lineTo(halfSize + lineGap, shape.width + lineGap); - - context.stroke(); - return context.createPattern(shape, "repeat"); - } - - function createComparativeColumnChart(canvasId, title, labelOne, labelTwo) { - var canvas = document.getElementById(canvasId); - if (!canvas) { - return - } - - var ctx = canvas.getContext("2d"); - - var listOne = JSON.parse(canvas.getAttribute('data-list-one')); - var listTwo = JSON.parse(canvas.getAttribute('data-list-two')); - - var data = { - labels: ["Total", "Federal", "Interstate", "State/Territory", "Tribal", "County", "City", "Special District", "School District", "Election Board"], - datasets: [ - { - label: labelOne, - backgroundColor: "rgba(255, 99, 132, 0.3)", - borderColor: "rgba(255, 99, 132, 1)", - borderWidth: 1, - data: listOne, - // Set this line style to be rightToLeft for visual distinction - backgroundColor: createDiagonalPattern('rgba(255, 99, 132, 0.3)', 'white', true) - }, - { - label: labelTwo, - backgroundColor: "rgba(75, 192, 192, 0.3)", - borderColor: "rgba(75, 192, 192, 1)", - borderWidth: 1, - data: listTwo, - backgroundColor: createDiagonalPattern('rgba(75, 192, 192, 0.3)', 'white') - }, - ], - }; - - var options = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'top', - }, - title: { - display: true, - text: title - } - }, - scales: { - y: { - beginAtZero: true, - }, - }, - }; - - if (chartInstances.has(canvasId)) { - chartInstances.get(canvasId).destroy(); - } - - const chart = new Chart(ctx, { - type: "bar", - data: data, - options: options, - }); - - chartInstances.set(canvasId, chart); - } - - function handleResize() { - // Debounce the resize handler - if (handleResize.timeout) { - clearTimeout(handleResize.timeout); - } - - handleResize.timeout = setTimeout(() => { - chartInstances.forEach((chart, canvasId) => { - if (chart && chart.canvas) { - chart.resize(); - } - }); - }, 100); - } - - function initComparativeColumnCharts() { - document.addEventListener("DOMContentLoaded", function () { - createComparativeColumnChart("myChart1", "Managed domains", "Start Date", "End Date"); - createComparativeColumnChart("myChart2", "Unmanaged domains", "Start Date", "End Date"); - createComparativeColumnChart("myChart3", "Deleted domains", "Start Date", "End Date"); - createComparativeColumnChart("myChart4", "Ready domains", "Start Date", "End Date"); - createComparativeColumnChart("myChart5", "Submitted requests", "Start Date", "End Date"); - createComparativeColumnChart("myChart6", "All requests", "Start Date", "End Date"); - - window.addEventListener("resize", handleResize); - }); - }; - - initComparativeColumnCharts(); -})(); diff --git a/src/registrar/assets/src/js/getgov-admin/analytics.js b/src/registrar/assets/src/js/getgov-admin/analytics.js new file mode 100644 index 000000000..d2808f623 --- /dev/null +++ b/src/registrar/assets/src/js/getgov-admin/analytics.js @@ -0,0 +1,196 @@ + +import { debounce } from '../getgov/helpers.js'; +import { getParameterByName } from './helpers-admin.js'; + + +/** This function also sets the start and end dates to match the url params if they exist +*/ +function initAnalyticsExportButtons() { + // Get the current date in the format YYYY-MM-DD + let currentDate = new Date().toISOString().split('T')[0]; + + // Default the value of the start date input field to the current date + let startDateInput = document.getElementById('start'); + + // Default the value of the end date input field to the current date + let endDateInput = document.getElementById('end'); + + let exportButtons = document.querySelectorAll('.exportLink'); + + if (exportButtons.length > 0) { + // Check if start and end dates are present in the URL + let urlStartDate = getParameterByName('start_date'); + let urlEndDate = getParameterByName('end_date'); + + // Set input values based on URL parameters or current date + startDateInput.value = urlStartDate || currentDate; + endDateInput.value = urlEndDate || currentDate; + + exportButtons.forEach((btn) => { + btn.addEventListener('click', function () { + // Get the selected start and end dates + let startDate = startDateInput.value; + let endDate = endDateInput.value; + let exportUrl = btn.dataset.exportUrl; + + // Build the URL with parameters + exportUrl += "?start_date=" + startDate + "&end_date=" + endDate; + + // Redirect to the export URL + window.location.href = exportUrl; + }); + }); + } + +}; + +/** + * Creates a diagonal stripe pattern for chart.js + * Inspired by https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns + * and https://github.com/ashiguruma/patternomaly + * @param {string} backgroundColor - Background color of the pattern + * @param {string} [lineColor="white"] - Color of the diagonal lines + * @param {boolean} [rightToLeft=false] - Direction of the diagonal lines + * @param {number} [lineGap=1] - Gap between lines + * @returns {CanvasPattern} A canvas pattern object for use with backgroundColor + */ +function createDiagonalPattern(backgroundColor, lineColor, rightToLeft=false, lineGap=1) { + // Define the canvas and the 2d context so we can draw on it + let shape = document.createElement("canvas"); + shape.width = 20; + shape.height = 20; + let context = shape.getContext("2d"); + + // Fill with specified background color + context.fillStyle = backgroundColor; + context.fillRect(0, 0, shape.width, shape.height); + + // Set stroke properties + context.strokeStyle = lineColor; + context.lineWidth = 2; + + // Rotate canvas for a right-to-left pattern + if (rightToLeft) { + context.translate(shape.width, 0); + context.rotate(90 * Math.PI / 180); + }; + + // First diagonal line + let halfSize = shape.width / 2; + context.moveTo(halfSize - lineGap, -lineGap); + context.lineTo(shape.width + lineGap, halfSize + lineGap); + + // Second diagonal line (x,y are swapped) + context.moveTo(-lineGap, halfSize - lineGap); + context.lineTo(halfSize + lineGap, shape.width + lineGap); + + context.stroke(); + return context.createPattern(shape, "repeat"); +} + +function createComparativeColumnChart(canvasId, title, labelOne, labelTwo, chartInstances) { + var canvas = document.getElementById(canvasId); + if (!canvas) { + return + } + + var ctx = canvas.getContext("2d"); + + var listOne = JSON.parse(canvas.getAttribute('data-list-one')); + var listTwo = JSON.parse(canvas.getAttribute('data-list-two')); + + var data = { + labels: ["Total", "Federal", "Interstate", "State/Territory", "Tribal", "County", "City", "Special District", "School District", "Election Board"], + datasets: [ + { + label: labelOne, + backgroundColor: "rgba(255, 99, 132, 0.3)", + borderColor: "rgba(255, 99, 132, 1)", + borderWidth: 1, + data: listOne, + // Set this line style to be rightToLeft for visual distinction + backgroundColor: createDiagonalPattern('rgba(255, 99, 132, 0.3)', 'white', true) + }, + { + label: labelTwo, + backgroundColor: "rgba(75, 192, 192, 0.3)", + borderColor: "rgba(75, 192, 192, 1)", + borderWidth: 1, + data: listTwo, + backgroundColor: createDiagonalPattern('rgba(75, 192, 192, 0.3)', 'white') + }, + ], + }; + + var options = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: title + } + }, + scales: { + y: { + beginAtZero: true, + }, + }, + }; + + if (chartInstances.has(canvasId)) { + chartInstances.get(canvasId).destroy(); + } + + const chart = new Chart(ctx, { + type: "bar", + data: data, + options: options, + }); + + chartInstances.set(canvasId, chart); +} + +function initComparativeColumnCharts(chartInstances) { + // Create charts + const charts = [ + { id: "managed-domains-chart", title: "Managed domains" }, + { id: "unmanaged-domains-chart", title: "Unmanaged domains" }, + { id: "deleted-domains-chart", title: "Deleted domains" }, + { id: "ready-domains-chart", title: "Ready domains" }, + { id: "submitted-requests-chart", title: "Submitted requests" }, + { id: "all-requests-chart", title: "All requests" } + ]; + charts.forEach(chart => { + createComparativeColumnChart( + chart.id, + chart.title, + "Start Date", + "End Date", + chartInstances + ); + }); + + // Add resize listener to each chart + window.addEventListener("resize", debounce(() => { + chartInstances.forEach((chart) => { + if (chart?.canvas) chart.resize(); + }); + }, 200)); +}; + +/** An IIFE to initialize the analytics page +*/ +export function initAnalyticsDashboard() { + const chartInstances = new Map(); + const analyticsPageContainer = document.querySelector('.analytics-dashboard .analytics-dashboard-charts'); + if (analyticsPageContainer) { + document.addEventListener("DOMContentLoaded", function () { + initAnalyticsExportButtons(); + initComparativeColumnCharts(chartInstances); + }); + } +}; diff --git a/src/registrar/assets/src/js/getgov-admin/helpers-admin.js b/src/registrar/assets/src/js/getgov-admin/helpers-admin.js index ff618a67d..8055e29d3 100644 --- a/src/registrar/assets/src/js/getgov-admin/helpers-admin.js +++ b/src/registrar/assets/src/js/getgov-admin/helpers-admin.js @@ -22,3 +22,13 @@ export function addOrRemoveSessionBoolean(name, add){ sessionStorage.removeItem(name); } } + +export function getParameterByName(name, url) { + if (!url) url = window.location.href; + name = name.replace(/[\[\]]/g, '\\$&'); + var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), + results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); +} diff --git a/src/registrar/assets/src/js/getgov-admin/main.js b/src/registrar/assets/src/js/getgov-admin/main.js index 64be572b2..5c6de20ab 100644 --- a/src/registrar/assets/src/js/getgov-admin/main.js +++ b/src/registrar/assets/src/js/getgov-admin/main.js @@ -15,6 +15,7 @@ import { initDomainFormTargetBlankButtons } from './domain-form.js'; import { initDynamicPortfolioFields } from './portfolio-form.js'; import { initDynamicDomainInformationFields } from './domain-information-form.js'; import { initDynamicDomainFields } from './domain-form.js'; +import { initAnalyticsDashboard } from './analytics.js'; // General initModals(); @@ -41,3 +42,6 @@ initDynamicPortfolioFields(); // Domain information initDynamicDomainInformationFields(); + +// Analytics dashboard +initAnalyticsDashboard(); diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index ca3501eec..d345aeb14 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -18,7 +18,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/ {% block content %} -
    +
    @@ -136,7 +136,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
    {% comment %} Managed/Unmanaged domains {% endcomment %}
    -
    - -
    - -
    - - {% endblock %} From b3a3dcad6dcfe15331f3da2dba69af58c450dce0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:31:01 -0700 Subject: [PATCH 113/139] Cleanup + code simplification --- .../assets/src/js/getgov-admin/analytics.js | 67 +++++++------------ .../admin/analytics_graph_table.html | 2 +- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/src/registrar/assets/src/js/getgov-admin/analytics.js b/src/registrar/assets/src/js/getgov-admin/analytics.js index d2808f623..e2de7b247 100644 --- a/src/registrar/assets/src/js/getgov-admin/analytics.js +++ b/src/registrar/assets/src/js/getgov-admin/analytics.js @@ -41,7 +41,6 @@ function initAnalyticsExportButtons() { }); }); } - }; /** @@ -88,8 +87,8 @@ function createDiagonalPattern(backgroundColor, lineColor, rightToLeft=false, li return context.createPattern(shape, "repeat"); } -function createComparativeColumnChart(canvasId, title, labelOne, labelTwo, chartInstances) { - var canvas = document.getElementById(canvasId); +function createComparativeColumnChart(id, title, labelOne, labelTwo) { + var canvas = document.getElementById(id); if (!canvas) { return } @@ -140,57 +139,43 @@ function createComparativeColumnChart(canvasId, title, labelOne, labelTwo, chart }, }, }; - - if (chartInstances.has(canvasId)) { - chartInstances.get(canvasId).destroy(); - } - - const chart = new Chart(ctx, { + return new Chart(ctx, { type: "bar", data: data, options: options, }); - - chartInstances.set(canvasId, chart); } -function initComparativeColumnCharts(chartInstances) { - // Create charts - const charts = [ - { id: "managed-domains-chart", title: "Managed domains" }, - { id: "unmanaged-domains-chart", title: "Unmanaged domains" }, - { id: "deleted-domains-chart", title: "Deleted domains" }, - { id: "ready-domains-chart", title: "Ready domains" }, - { id: "submitted-requests-chart", title: "Submitted requests" }, - { id: "all-requests-chart", title: "All requests" } - ]; - charts.forEach(chart => { - createComparativeColumnChart( - chart.id, - chart.title, - "Start Date", - "End Date", - chartInstances - ); - }); - - // Add resize listener to each chart - window.addEventListener("resize", debounce(() => { - chartInstances.forEach((chart) => { - if (chart?.canvas) chart.resize(); - }); - }, 200)); -}; - /** An IIFE to initialize the analytics page */ export function initAnalyticsDashboard() { - const chartInstances = new Map(); const analyticsPageContainer = document.querySelector('.analytics-dashboard .analytics-dashboard-charts'); if (analyticsPageContainer) { document.addEventListener("DOMContentLoaded", function () { initAnalyticsExportButtons(); - initComparativeColumnCharts(chartInstances); + + // Create charts and store each instance of it + const chartInstances = new Map(); + const charts = [ + { id: "managed-domains-chart", title: "Managed domains" }, + { id: "unmanaged-domains-chart", title: "Unmanaged domains" }, + { id: "deleted-domains-chart", title: "Deleted domains" }, + { id: "ready-domains-chart", title: "Ready domains" }, + { id: "submitted-requests-chart", title: "Submitted requests" }, + { id: "all-requests-chart", title: "All requests" } + ]; + charts.forEach(chart => { + if (chartInstances.has(chart.id)) chartInstances.get(chart.id).destroy(); + let chart = createComparativeColumnChart(...chart, "Start Date", "End Date"); + chartInstances.set(chart.id, chart); + }); + + // Add resize listener to each chart + window.addEventListener("resize", debounce(() => { + chartInstances.forEach((chart) => { + if (chart?.canvas) chart.resize(); + }); + }, 200)); }); } }; diff --git a/src/registrar/templates/admin/analytics_graph_table.html b/src/registrar/templates/admin/analytics_graph_table.html index 88b538745..5f10da93a 100644 --- a/src/registrar/templates/admin/analytics_graph_table.html +++ b/src/registrar/templates/admin/analytics_graph_table.html @@ -23,4 +23,4 @@ {% endfor %} {% endwith %}
  • -
    TypeStart date {{ data.start_date }}End date {{ data.end_date }} TypeStart date {{ data.start_date }}End date {{ data.end_date }}
    - - - ${domain.action_label} ${domain.name} - +
    \ No newline at end of file + From 62a8a0c2a1e5fbd6cc72de262d0267c566a89681 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:34:38 -0700 Subject: [PATCH 114/139] Update analytics.js --- src/registrar/assets/src/js/getgov-admin/analytics.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/assets/src/js/getgov-admin/analytics.js b/src/registrar/assets/src/js/getgov-admin/analytics.js index e2de7b247..7bf6a05b8 100644 --- a/src/registrar/assets/src/js/getgov-admin/analytics.js +++ b/src/registrar/assets/src/js/getgov-admin/analytics.js @@ -1,8 +1,6 @@ - import { debounce } from '../getgov/helpers.js'; import { getParameterByName } from './helpers-admin.js'; - /** This function also sets the start and end dates to match the url params if they exist */ function initAnalyticsExportButtons() { @@ -94,7 +92,6 @@ function createComparativeColumnChart(id, title, labelOne, labelTwo) { } var ctx = canvas.getContext("2d"); - var listOne = JSON.parse(canvas.getAttribute('data-list-one')); var listTwo = JSON.parse(canvas.getAttribute('data-list-two')); From 8a0fdc7ea637053c532d387d58c0afef0698551b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:44:22 -0700 Subject: [PATCH 115/139] Update test_reports.py --- src/registrar/tests/test_reports.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index bab4f327b..18c98807d 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -1,4 +1,5 @@ import io +from unittest import skip from django.test import Client, RequestFactory from io import StringIO from registrar.models import ( @@ -819,6 +820,7 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib): super().setUp() self.factory = RequestFactory() + @skip("flaky test that needs to be refactored") @override_flag("organization_feature", active=True) @override_flag("organization_members", active=True) @less_console_noise_decorator From c540a324c7df33c7011fcef4bebe158a43bf417a Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 12 Feb 2025 10:11:25 -0800 Subject: [PATCH 116/139] Add unit tests --- src/registrar/tests/test_emails.py | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py index f39f11517..c79038668 100644 --- a/src/registrar/tests/test_emails.py +++ b/src/registrar/tests/test_emails.py @@ -108,6 +108,82 @@ class TestEmails(TestCase): self.assertEqual(["testy2@town.com", "mayor@igorville.gov"], kwargs["Destination"]["CcAddresses"]) + @boto3_mocking.patching + @override_settings(IS_PRODUCTION=True, BASE_URL="manage.get.gov") + def test_email_production_subject_and_url_check(self): + """Test sending an email in production that: + 1. Does not have a prefix in the email subject (no [MANAGE]) + 2. Uses the production URL in the email body of manage.get.gov still""" + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + send_templated_email( + "emails/update_to_approved_domain.txt", + "emails/update_to_approved_domain_subject.txt", + "doesnotexist@igorville.com", + context={"domain": "test", "user": "test", "date": 1, "changes": "test"}, + bcc_address=None, + cc_addresses=["testy2@town.com", "mayor@igorville.gov"], + ) + + # check that an email was sent + self.assertTrue(self.mock_client.send_email.called) + + # check the call sequence for the email + args, kwargs = self.mock_client.send_email.call_args + self.assertIn("Destination", kwargs) + self.assertIn("CcAddresses", kwargs["Destination"]) + + self.assertEqual(["testy2@town.com", "mayor@igorville.gov"], kwargs["Destination"]["CcAddresses"]) + + # Grab email subject + email_subject = kwargs["Content"]["Simple"]["Subject"]["Data"] + + # Check that the subject does NOT contain a prefix for production + self.assertNotIn("[MANAGE]", email_subject) + self.assertIn("An update was made to", email_subject) + + # Grab email body + email_body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + + # Check that manage_url is correctly set for production + self.assertIn("https://manage.get.gov", email_body) + + @boto3_mocking.patching + @override_settings(IS_PRODUCTION=False, BASE_URL="https://getgov-rh.app.cloud.gov") + def test_email_non_production_subject_and_url_check(self): + """Test sending an email in production that: + 1. Does prefix in the email subject ([GETGOV-RH]) + 2. Uses the sandbox url in the email body (ie getgov-rh.app.cloud.gov)""" + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + send_templated_email( + "emails/update_to_approved_domain.txt", + "emails/update_to_approved_domain_subject.txt", + "doesnotexist@igorville.com", + context={"domain": "test", "user": "test", "date": 1, "changes": "test"}, + bcc_address=None, + cc_addresses=["testy2@town.com", "mayor@igorville.gov"], + ) + + # check that an email was sent + self.assertTrue(self.mock_client.send_email.called) + + # check the call sequence for the email + args, kwargs = self.mock_client.send_email.call_args + self.assertIn("Destination", kwargs) + self.assertIn("CcAddresses", kwargs["Destination"]) + self.assertEqual(["testy2@town.com", "mayor@igorville.gov"], kwargs["Destination"]["CcAddresses"]) + + # Grab email subject + email_subject = kwargs["Content"]["Simple"]["Subject"]["Data"] + + # Check that the subject DOES contain a prefix of the current sandbox + self.assertIn("[GETGOV-RH]", email_subject) + + # Grab email body + email_body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + + # Check that manage_url is correctly set of the sandbox + self.assertIn("https://getgov-rh.app.cloud.gov", email_body) + @boto3_mocking.patching @less_console_noise_decorator def test_submission_confirmation(self): From 0454e7295136eedc10a3190f09bc90b95ca899f2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:23:37 -0700 Subject: [PATCH 117/139] fix typo --- src/registrar/assets/src/js/getgov-admin/analytics.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/assets/src/js/getgov-admin/analytics.js b/src/registrar/assets/src/js/getgov-admin/analytics.js index 7bf6a05b8..a5488fa3d 100644 --- a/src/registrar/assets/src/js/getgov-admin/analytics.js +++ b/src/registrar/assets/src/js/getgov-admin/analytics.js @@ -163,8 +163,7 @@ export function initAnalyticsDashboard() { ]; charts.forEach(chart => { if (chartInstances.has(chart.id)) chartInstances.get(chart.id).destroy(); - let chart = createComparativeColumnChart(...chart, "Start Date", "End Date"); - chartInstances.set(chart.id, chart); + chartInstances.set(chart.id, createComparativeColumnChart(...chart, "Start Date", "End Date")); }); // Add resize listener to each chart From 0725ab8e59e59ac63267fe1d008998d7fd7ba26c Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 12 Feb 2025 10:31:53 -0800 Subject: [PATCH 118/139] Clean up the comments --- src/registrar/tests/test_emails.py | 2 +- src/registrar/utility/email.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py index c79038668..2b7f89ac9 100644 --- a/src/registrar/tests/test_emails.py +++ b/src/registrar/tests/test_emails.py @@ -151,7 +151,7 @@ class TestEmails(TestCase): @override_settings(IS_PRODUCTION=False, BASE_URL="https://getgov-rh.app.cloud.gov") def test_email_non_production_subject_and_url_check(self): """Test sending an email in production that: - 1. Does prefix in the email subject ([GETGOV-RH]) + 1. Does prefix in the email subject (ie [GETGOV-RH]) 2. Uses the sandbox url in the email body (ie getgov-rh.app.cloud.gov)""" with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): send_templated_email( diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 535096b10..9323255af 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -57,7 +57,7 @@ def send_templated_email( # noqa env_name = re.sub(r"^https?://", "", env_base_url).split(".")[0] # To add to subject lines ie [GETGOV-RH] prefix = f"[{env_name.upper()}] " if not settings.IS_PRODUCTION else "" - # For email links + # For email links ie getgov-rh.app.cloud.gov manage_url = env_base_url if not settings.IS_PRODUCTION else "https://manage.get.gov" # Adding to context From 00732d0a64499224cc1e7da96ec35eba54970d51 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 12 Feb 2025 10:58:38 -0800 Subject: [PATCH 119/139] Fix carrot link --- src/registrar/templates/emails/domain_manager_notification.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/domain_manager_notification.txt b/src/registrar/templates/emails/domain_manager_notification.txt index 18e682329..b5096a9d8 100644 --- a/src/registrar/templates/emails/domain_manager_notification.txt +++ b/src/registrar/templates/emails/domain_manager_notification.txt @@ -15,7 +15,7 @@ The person who received the invitation will become a domain manager once they lo associated with the invited email address. If you need to cancel this invitation or remove the domain manager, you can do that by going to -this domain in the .gov registrar <{{ manage_url }}. +this domain in the .gov registrar <{{ manage_url }}>. WHY DID YOU RECEIVE THIS EMAIL? From 16f0ae6f627417f9162c29bfa7b832396e4c5951 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 12 Feb 2025 11:00:13 -0800 Subject: [PATCH 120/139] Fix more spacing --- .../emails/portfolio_admin_removal_notification_subject.txt | 2 +- src/registrar/templates/emails/update_to_approved_domain.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt index 030d27ae7..9a45a8bbc 100644 --- a/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt +++ b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt @@ -1 +1 @@ -{{ prefix}}An admin was removed from your .gov organization \ No newline at end of file +{{ prefix }}An admin was removed from your .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/update_to_approved_domain.txt b/src/registrar/templates/emails/update_to_approved_domain.txt index fb0a442cb..070096f62 100644 --- a/src/registrar/templates/emails/update_to_approved_domain.txt +++ b/src/registrar/templates/emails/update_to_approved_domain.txt @@ -1,4 +1,4 @@ - {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} +{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} Hi, An update was made to a domain you manage. From 53674dea6ab674ad6a491e73a2c7f7c85ef4b9b4 Mon Sep 17 00:00:00 2001 From: lizpearl Date: Wed, 12 Feb 2025 15:20:38 -0600 Subject: [PATCH 121/139] Add Jaxon to fixtures --- src/registrar/fixtures/fixtures_users.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/registrar/fixtures/fixtures_users.py b/src/registrar/fixtures/fixtures_users.py index 876bc9fb5..fdaa1c135 100644 --- a/src/registrar/fixtures/fixtures_users.py +++ b/src/registrar/fixtures/fixtures_users.py @@ -171,6 +171,13 @@ class UserFixture: "email": "gina.summers@ecstech.com", "title": "Scrum Master", }, + { + "username": "89f2db87-87a2-4778-a5ea-5b27b585b131", + "first_name": "Jaxon", + "last_name": "Silva", + "email": "jaxon.silva@cisa.dhs.gov", + "title": "Designer", + }, ] STAFF = [ From 6a44fe97aa7baf573ac19cb5a2996bd453d415b9 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Wed, 12 Feb 2025 22:05:23 -0500 Subject: [PATCH 122/139] fixed pr comments --- src/registrar/models/domain.py | 2 +- src/registrar/views/domain.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 245f869ce..d92da8832 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1172,7 +1172,7 @@ class Domain(TimeStampedModel, DomainHelper): """Return the display status of the domain.""" if self.is_expired() and (self.state != self.State.UNKNOWN): return "Expired" - elif flag_is_active(request, "domain_renewal") and self.is_expiring(): + elif self.is_expiring(): return "Expiring soon" elif self.state == self.State.UNKNOWN or self.state == self.State.DNS_NEEDED: return "DNS needed" diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index b6cabfcf6..72826e570 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -366,7 +366,7 @@ class DomainRenewalView(DomainBaseView): return HttpResponseRedirect(reverse("domain", kwargs={"pk": pk})) # if not valid, render the template with error messages - # passing editable,å and is_editable for re-render + # passing editable and is_editable for re-render return render( request, "domain_renewal.html", From 3245cded0e81597974496799eace8a3ee6835969 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Thu, 13 Feb 2025 10:58:40 -0500 Subject: [PATCH 123/139] updated naming for workflow --- .github/workflows/delete-and-recreate-db.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/delete-and-recreate-db.yaml b/.github/workflows/delete-and-recreate-db.yaml index ecdf54bbc..e7cad783d 100644 --- a/.github/workflows/delete-and-recreate-db.yaml +++ b/.github/workflows/delete-and-recreate-db.yaml @@ -1,8 +1,8 @@ # This workflow can be run from the CLI # gh workflow run reset-db.yaml -f environment=ENVIRONMENT -name: Reset database -run-name: Reset database for ${{ github.event.inputs.environment }} +name: Delete and Recreate database +run-name: Delete and Recreate for ${{ github.event.inputs.environment }} on: workflow_dispatch: From 71a9e9515d99d5e4481c1993379bbeb74c5b178f Mon Sep 17 00:00:00 2001 From: asaki222 Date: Thu, 13 Feb 2025 11:09:32 -0500 Subject: [PATCH 124/139] updated the creds variable --- .github/workflows/delete-and-recreate-db.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/delete-and-recreate-db.yaml b/.github/workflows/delete-and-recreate-db.yaml index e7cad783d..979f20826 100644 --- a/.github/workflows/delete-and-recreate-db.yaml +++ b/.github/workflows/delete-and-recreate-db.yaml @@ -53,7 +53,7 @@ jobs: sudo apt-get update sudo apt-get install cf8-cli cf api api.fr.cloud.gov - cf auth "$CF_USERNAME" "$CF_PASSWORD" + cf auth "$cf_username" "$cf_password" cf target -o cisa-dotgov -s $DESTINATION_ENVIRONMENT From a928cc742c221579a955de8b9261f3b98335dbc8 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Thu, 13 Feb 2025 12:18:55 -0500 Subject: [PATCH 125/139] updated changes --- src/registrar/context_processors.py | 8 -------- src/registrar/models/domain.py | 1 - 2 files changed, 9 deletions(-) diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 061c0ab4f..4e17b7fa1 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -71,14 +71,6 @@ def portfolio_permissions(request): } try: portfolio = request.session.get("portfolio") - - # These feature flags will display and doesn't depend on portfolio - portfolio_context.update( - { - "has_organization_feature_flag": True, - } - ) - if portfolio: return { "has_view_portfolio_permission": request.user.has_view_portfolio_permission(portfolio), diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index d92da8832..42310c3bb 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -41,7 +41,6 @@ from .utility.time_stamped_model import TimeStampedModel from .public_contact import PublicContact from .user_domain_role import UserDomainRole -from waffle.decorators import flag_is_active logger = logging.getLogger(__name__) From c737daa8fd2f8d497edfb9c5499d74e66316dcd7 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 13 Feb 2025 10:28:49 -0800 Subject: [PATCH 126/139] Add prefix and manage url to the action needed reasons templates --- .../action_needed_reasons/already_has_domains_subject.txt | 2 +- .../templates/emails/action_needed_reasons/bad_name.txt | 2 +- .../templates/emails/action_needed_reasons/bad_name_subject.txt | 2 +- .../action_needed_reasons/eligibility_unclear_subject.txt | 2 +- .../action_needed_reasons/questionable_senior_official.txt | 2 +- .../questionable_senior_official_subject.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt b/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt index 7ca332ddd..b29b8040c 100644 --- a/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/action_needed_reasons/bad_name.txt b/src/registrar/templates/emails/action_needed_reasons/bad_name.txt index ac563b549..40e5ed899 100644 --- a/src/registrar/templates/emails/action_needed_reasons/bad_name.txt +++ b/src/registrar/templates/emails/action_needed_reasons/bad_name.txt @@ -17,7 +17,7 @@ Domains should uniquely identify a government organization and be clear to the g ACTION NEEDED -First, we need you to identify a new domain name that meets our naming requirements for your type of organization. Then, log in to the registrar and update the name in your domain request. Once you submit your updated request, we’ll resume the adjudication process. +First, we need you to identify a new domain name that meets our naming requirements for your type of organization. Then, log in to the registrar and update the name in your domain request. <{{ manage_url }}> Once you submit your updated request, we’ll resume the adjudication process. If you have questions or want to discuss potential domain names, reply to this email. diff --git a/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt b/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt index 7ca332ddd..b29b8040c 100644 --- a/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt index 7ca332ddd..b29b8040c 100644 --- a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt index ef05e17d7..40d068cd9 100644 --- a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt +++ b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt @@ -21,7 +21,7 @@ We expect a senior official to be someone in a role of significant, executive re ACTION NEEDED Reply to this email with a justification for naming {{ domain_request.senior_official.get_formatted_name }} as the senior official. If you have questions or comments, include those in your reply. -Alternatively, you can log in to the registrar and enter a different senior official for this domain request. Once you submit your updated request, we’ll resume the adjudication process. +Alternatively, you can log in to the registrar and enter a different senior official for this domain request. <{{ manage_url }}> Once you submit your updated request, we’ll resume the adjudication process. THANK YOU diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt index 7ca332ddd..b29b8040c 100644 --- a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file From 856b71b6ce70e429a77a1342e20ac41db8a75b42 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 13 Feb 2025 10:30:48 -0800 Subject: [PATCH 127/139] Add for transition domain inv email --- src/registrar/templates/emails/transition_domain_invitation.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/transition_domain_invitation.txt b/src/registrar/templates/emails/transition_domain_invitation.txt index dc812edf3..14dd626dd 100644 --- a/src/registrar/templates/emails/transition_domain_invitation.txt +++ b/src/registrar/templates/emails/transition_domain_invitation.txt @@ -57,7 +57,7 @@ THANK YOU The .gov team .Gov blog -Domain management +Domain management <{{ manage_url }}}> Get.gov The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) From 986bcc235125f627c6458a4b36b5d40ad227e106 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 14 Feb 2025 06:56:56 -0500 Subject: [PATCH 128/139] fixed date in email --- src/registrar/utility/email_invitations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index d206bf279..7ddab65f1 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -255,6 +255,7 @@ def send_portfolio_member_permission_update_email(requestor, permissions: UserPo "portfolio": permissions.portfolio, "requestor_email": requestor_email, "permissions": permissions, + "date": date.today(), }, ) except EmailSendingError: From 8020bd6f7461b71fa663fc5fff8c4a797eeed907 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 14 Feb 2025 08:02:47 -0800 Subject: [PATCH 129/139] Update for prefix to be in the subject dynamically --- .../already_has_domains_subject.txt | 2 +- .../emails/action_needed_reasons/bad_name_subject.txt | 2 +- .../eligibility_unclear_subject.txt | 2 +- .../questionable_senior_official_subject.txt | 2 +- .../templates/emails/domain_invitation_subject.txt | 2 +- .../domain_manager_deleted_notification_subject.txt | 2 +- .../emails/domain_manager_notification_subject.txt | 2 +- .../emails/domain_request_withdrawn_subject.txt | 2 +- src/registrar/templates/emails/metadata_body.txt | 2 +- src/registrar/templates/emails/metadata_subject.txt | 2 +- .../portfolio_admin_addition_notification_subject.txt | 2 +- .../portfolio_admin_removal_notification_subject.txt | 2 +- .../templates/emails/portfolio_invitation_subject.txt | 2 +- .../emails/status_change_approved_subject.txt | 2 +- .../templates/emails/status_change_subject.txt | 2 +- .../emails/submission_confirmation_subject.txt | 2 +- .../emails/transition_domain_invitation_subject.txt | 2 +- .../emails/update_to_approved_domain_subject.txt | 2 +- src/registrar/utility/email.py | 11 +++++++---- 19 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt b/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt index b29b8040c..7ca332ddd 100644 --- a/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt b/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt index b29b8040c..7ca332ddd 100644 --- a/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt index b29b8040c..7ca332ddd 100644 --- a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt index b29b8040c..7ca332ddd 100644 --- a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_invitation_subject.txt b/src/registrar/templates/emails/domain_invitation_subject.txt index 9f15c38b4..9663346d0 100644 --- a/src/registrar/templates/emails/domain_invitation_subject.txt +++ b/src/registrar/templates/emails/domain_invitation_subject.txt @@ -1 +1 @@ -{{ prefix }}You've been invited to manage {% if domains|length > 1 %}.gov domains{% else %}{{ domains.0.name }}{% endif %} \ No newline at end of file +You've been invited to manage {% if domains|length > 1 %}.gov domains{% else %}{{ domains.0.name }}{% endif %} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt b/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt index 7376bdb86..c84a20f18 100644 --- a/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt +++ b/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt @@ -1 +1 @@ -{{ prefix }}A domain manager was removed from {{ domain.name }} \ No newline at end of file +A domain manager was removed from {{ domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_manager_notification_subject.txt b/src/registrar/templates/emails/domain_manager_notification_subject.txt index 8560cb9fa..0e9918de0 100644 --- a/src/registrar/templates/emails/domain_manager_notification_subject.txt +++ b/src/registrar/templates/emails/domain_manager_notification_subject.txt @@ -1 +1 @@ -{{ prefix }}A domain manager was invited to {{ domain.name }} \ No newline at end of file +A domain manager was invited to {{ domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_request_withdrawn_subject.txt b/src/registrar/templates/emails/domain_request_withdrawn_subject.txt index cc146643a..51b2c745a 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn_subject.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} +Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/metadata_body.txt b/src/registrar/templates/emails/metadata_body.txt index a0a3682b7..adf0a186c 100644 --- a/src/registrar/templates/emails/metadata_body.txt +++ b/src/registrar/templates/emails/metadata_body.txt @@ -1 +1 @@ -{{ prefix }}An export of all .gov metadata. +An export of all .gov metadata. diff --git a/src/registrar/templates/emails/metadata_subject.txt b/src/registrar/templates/emails/metadata_subject.txt index c19b4c26e..5fdece7ef 100644 --- a/src/registrar/templates/emails/metadata_subject.txt +++ b/src/registrar/templates/emails/metadata_subject.txt @@ -1,2 +1,2 @@ -{{ prefix }}Domain metadata - {{current_date_str}} +Domain metadata - {{current_date_str}} diff --git a/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt b/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt index ee5987512..3d6b2a140 100644 --- a/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt +++ b/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt @@ -1 +1 @@ -{{ prefix }}An admin was invited to your .gov organization \ No newline at end of file +An admin was invited to your .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt index 9a45a8bbc..e250b17f8 100644 --- a/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt +++ b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt @@ -1 +1 @@ -{{ prefix }}An admin was removed from your .gov organization \ No newline at end of file +An admin was removed from your .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/portfolio_invitation_subject.txt b/src/registrar/templates/emails/portfolio_invitation_subject.txt index de9080196..552bb2bec 100644 --- a/src/registrar/templates/emails/portfolio_invitation_subject.txt +++ b/src/registrar/templates/emails/portfolio_invitation_subject.txt @@ -1 +1 @@ -{{ prefix }}You’ve been invited to a .gov organization \ No newline at end of file +You’ve been invited to a .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/status_change_approved_subject.txt b/src/registrar/templates/emails/status_change_approved_subject.txt index cc146643a..51b2c745a 100644 --- a/src/registrar/templates/emails/status_change_approved_subject.txt +++ b/src/registrar/templates/emails/status_change_approved_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} +Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/status_change_subject.txt b/src/registrar/templates/emails/status_change_subject.txt index cc146643a..51b2c745a 100644 --- a/src/registrar/templates/emails/status_change_subject.txt +++ b/src/registrar/templates/emails/status_change_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} +Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/submission_confirmation_subject.txt b/src/registrar/templates/emails/submission_confirmation_subject.txt index cc146643a..51b2c745a 100644 --- a/src/registrar/templates/emails/submission_confirmation_subject.txt +++ b/src/registrar/templates/emails/submission_confirmation_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} +Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/transition_domain_invitation_subject.txt b/src/registrar/templates/emails/transition_domain_invitation_subject.txt index b162341d9..526c7714b 100644 --- a/src/registrar/templates/emails/transition_domain_invitation_subject.txt +++ b/src/registrar/templates/emails/transition_domain_invitation_subject.txt @@ -1 +1 @@ -{{ prefix }}(Action required) Manage your .gov domain{% if domains|length > 1 %}s{% endif %} in the new registrar \ No newline at end of file +(Action required) Manage your .gov domain{% if domains|length > 1 %}s{% endif %} in the new registrar \ No newline at end of file diff --git a/src/registrar/templates/emails/update_to_approved_domain_subject.txt b/src/registrar/templates/emails/update_to_approved_domain_subject.txt index d952999a0..cf4c9a14c 100644 --- a/src/registrar/templates/emails/update_to_approved_domain_subject.txt +++ b/src/registrar/templates/emails/update_to_approved_domain_subject.txt @@ -1 +1 @@ -{{ prefix }}An update was made to {{domain}} \ No newline at end of file +An update was made to {{domain}} \ No newline at end of file diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 9323255af..b4caf42a5 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -60,13 +60,19 @@ def send_templated_email( # noqa # For email links ie getgov-rh.app.cloud.gov manage_url = env_base_url if not settings.IS_PRODUCTION else "https://manage.get.gov" + # Update the subject to have prefix here versus every email + subject_template = get_template(subject_template_name) + subject = subject_template.render(context=context) + subject = f"{prefix}{subject}" + # Adding to context context.update( { - "prefix": prefix, + "subject": subject, "manage_url": manage_url, } ) + # by default assume we can send to all addresses (prod has no whitelist) sendable_cc_addresses = cc_addresses @@ -89,9 +95,6 @@ def send_templated_email( # noqa if email_body: email_body.strip().lstrip("\n") - subject_template = get_template(subject_template_name) - subject = subject_template.render(context=context) - try: ses_client = boto3.client( "sesv2", From 33bf1988f1700ca149403555e3125697199c9128 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 14 Feb 2025 08:28:50 -0800 Subject: [PATCH 130/139] Fix placement of where email subject is edited --- src/registrar/utility/email.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index b4caf42a5..39c7f21ac 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -60,18 +60,7 @@ def send_templated_email( # noqa # For email links ie getgov-rh.app.cloud.gov manage_url = env_base_url if not settings.IS_PRODUCTION else "https://manage.get.gov" - # Update the subject to have prefix here versus every email - subject_template = get_template(subject_template_name) - subject = subject_template.render(context=context) - subject = f"{prefix}{subject}" - - # Adding to context - context.update( - { - "subject": subject, - "manage_url": manage_url, - } - ) + context["manage_url"] = manage_url # by default assume we can send to all addresses (prod has no whitelist) sendable_cc_addresses = cc_addresses @@ -95,6 +84,13 @@ def send_templated_email( # noqa if email_body: email_body.strip().lstrip("\n") + # Update the subject to have prefix here versus every email + subject_template = get_template(subject_template_name) + subject = subject_template.render(context=context) + subject = f"{prefix}{subject}" + + context["subject"] = subject + try: ses_client = boto3.client( "sesv2", From c81c229be23e3883e445ecfaba3f7ff01b2ba347 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 14 Feb 2025 12:01:25 -0500 Subject: [PATCH 131/139] fixed test --- src/registrar/tests/test_email_invitations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/tests/test_email_invitations.py b/src/registrar/tests/test_email_invitations.py index 20ac4a565..981bca6dd 100644 --- a/src/registrar/tests/test_email_invitations.py +++ b/src/registrar/tests/test_email_invitations.py @@ -919,6 +919,7 @@ class TestSendPortfolioMemberPermissionUpdateEmail(unittest.TestCase): "portfolio": permissions.portfolio, "requestor_email": "requestor@example.com", "permissions": permissions, + "date": date.today(), }, ) self.assertTrue(result) From 7d8249923e88a0fa4571d6290e5409d792bb42eb Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 14 Feb 2025 09:17:28 -0800 Subject: [PATCH 132/139] Update comments --- src/registrar/utility/email.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 39c7f21ac..797ad4aa9 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -53,11 +53,13 @@ def send_templated_email( # noqa context = {} env_base_url = settings.BASE_URL - # The regular expresstion is to get both http (localhost) and https (everything else) + # The regular expression is to get both http (localhost) and https (everything else) env_name = re.sub(r"^https?://", "", env_base_url).split(".")[0] - # To add to subject lines ie [GETGOV-RH] + # If NOT in prod, add env to the subject line + # IE adds [GETGOV-RH] if we are in the -RH sandbox prefix = f"[{env_name.upper()}] " if not settings.IS_PRODUCTION else "" - # For email links ie getgov-rh.app.cloud.gov + # If NOT in prod, update instances of "manage.get.gov" links to point to + # current environment, ie "getgov-rh.app.cloud.gov" manage_url = env_base_url if not settings.IS_PRODUCTION else "https://manage.get.gov" context["manage_url"] = manage_url From 026326b6eca501f9d00782c02ac7a947ac9338f6 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 14 Feb 2025 11:15:14 -0800 Subject: [PATCH 133/139] Remove unneeded additional context udpate --- src/registrar/utility/email.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 797ad4aa9..94e87a96b 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -91,8 +91,6 @@ def send_templated_email( # noqa subject = subject_template.render(context=context) subject = f"{prefix}{subject}" - context["subject"] = subject - try: ses_client = boto3.client( "sesv2", From 16ed7546fd19db1963fc37bb0f260a690f06df9e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 14 Feb 2025 12:35:00 -0700 Subject: [PATCH 134/139] Update analytics.js --- src/registrar/assets/src/js/getgov-admin/analytics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/src/js/getgov-admin/analytics.js b/src/registrar/assets/src/js/getgov-admin/analytics.js index a5488fa3d..7524b726f 100644 --- a/src/registrar/assets/src/js/getgov-admin/analytics.js +++ b/src/registrar/assets/src/js/getgov-admin/analytics.js @@ -163,7 +163,7 @@ export function initAnalyticsDashboard() { ]; charts.forEach(chart => { if (chartInstances.has(chart.id)) chartInstances.get(chart.id).destroy(); - chartInstances.set(chart.id, createComparativeColumnChart(...chart, "Start Date", "End Date")); + chartInstances.set(chart.id, createComparativeColumnChart(chart.id, chart.title, "Start Date", "End Date")); }); // Add resize listener to each chart From 62626636aeba4a03ff7520ffd4b66143e7090a06 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Sat, 15 Feb 2025 08:16:17 -0500 Subject: [PATCH 135/139] Viewer, all => Viewer --- src/registrar/assets/src/js/getgov/portfolio-member-page.js | 2 +- src/registrar/forms/portfolio.py | 2 +- src/registrar/models/portfolio_invitation.py | 2 +- src/registrar/models/user_portfolio_permission.py | 2 +- src/registrar/models/utility/portfolio_helper.py | 4 ++-- src/registrar/tests/test_views_portfolio.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/registrar/assets/src/js/getgov/portfolio-member-page.js b/src/registrar/assets/src/js/getgov/portfolio-member-page.js index c96677ebc..95723fc7e 100644 --- a/src/registrar/assets/src/js/getgov/portfolio-member-page.js +++ b/src/registrar/assets/src/js/getgov/portfolio-member-page.js @@ -128,7 +128,7 @@ export function initAddNewMemberPageListeners() { }); } else { // for admin users, the permissions are always the same - appendPermissionInContainer('Domains', 'Viewer, all', permissionDetailsContainer); + appendPermissionInContainer('Domains', 'Viewer', permissionDetailsContainer); appendPermissionInContainer('Domain requests', 'Creator', permissionDetailsContainer); appendPermissionInContainer('Members', 'Manager', permissionDetailsContainer); } diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index 3a9074b2d..9824ed68a 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -127,7 +127,7 @@ class BasePortfolioMemberForm(forms.ModelForm): domain_permissions = forms.ChoiceField( choices=[ (UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, "Viewer, limited"), - (UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, "Viewer, all"), + (UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, "Viewer"), ], widget=forms.RadioSelect, required=False, diff --git a/src/registrar/models/portfolio_invitation.py b/src/registrar/models/portfolio_invitation.py index 045cf2de4..99febc92e 100644 --- a/src/registrar/models/portfolio_invitation.py +++ b/src/registrar/models/portfolio_invitation.py @@ -108,7 +108,7 @@ class PortfolioInvitation(TimeStampedModel): Returns a string representation of the user's domain access level. Uses the `get_domains_display` function to determine whether the user has - "Viewer, all" access (can view all domains) or "Viewer, limited" access. + "Viewer" access (can view all domains) or "Viewer, limited" access. Returns: str: The display name of the user's domain permissions. diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 202fc2e8d..e077daa57 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -204,7 +204,7 @@ class UserPortfolioPermission(TimeStampedModel): Returns a string representation of the user's domain access level. Uses the `get_domains_display` function to determine whether the user has - "Viewer, all" access (can view all domains) or "Viewer, limited" access. + "Viewer" access (can view all domains) or "Viewer, limited" access. Returns: str: The display name of the user's domain permissions. diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py index 00c5ce2b8..03733237e 100644 --- a/src/registrar/models/utility/portfolio_helper.py +++ b/src/registrar/models/utility/portfolio_helper.py @@ -105,7 +105,7 @@ def get_domains_display(roles, permissions): """ Determines the display name for a user's domain viewing permissions. - - If the user has the VIEW_ALL_DOMAINS permission, return "Viewer, all". + - If the user has the VIEW_ALL_DOMAINS permission, return "Viewer". - Otherwise, return "Viewer, limited". Args: @@ -118,7 +118,7 @@ def get_domains_display(roles, permissions): UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission") all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions) if UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS in all_permissions: - return "Viewer, all" + return "Viewer" else: return "Viewer, limited" diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 65e0350ee..3ce1cfdfa 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1063,7 +1063,7 @@ class TestPortfolio(WebTest): self.assertContains(response, "Invited") self.assertContains(response, portfolio_invitation.email) self.assertContains(response, "Admin") - self.assertContains(response, "Viewer, all") + self.assertContains(response, "Viewer") self.assertContains(response, "Creator") self.assertContains(response, "Manager") self.assertContains( From 1b7871c4063233d8a8b8e6f5f13164edf5393eb4 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Sun, 16 Feb 2025 18:56:20 -0700 Subject: [PATCH 136/139] stripped dead code --- src/registrar/models/user.py | 45 +-------------- src/registrar/templatetags/custom_filters.py | 7 --- src/registrar/tests/test_models.py | 61 -------------------- 3 files changed, 1 insertion(+), 112 deletions(-) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 82a0465c5..d5476ab9a 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -269,7 +269,7 @@ class User(AbstractUser): return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS) def is_portfolio_admin(self, portfolio): - return "Admin" in self.portfolio_role_summary(portfolio) + return self.has_edit_portfolio_permission(portfolio) def get_first_portfolio(self): permission = self.portfolio_permissions.first() @@ -277,49 +277,6 @@ class User(AbstractUser): return permission.portfolio return None - def portfolio_role_summary(self, portfolio): - """Returns a list of roles based on the user's permissions.""" - roles = [] - - # Define the conditions and their corresponding roles - conditions_roles = [ - (self.has_edit_portfolio_permission(portfolio), ["Admin"]), - ( - self.has_view_all_domains_portfolio_permission(portfolio) - and self.has_any_requests_portfolio_permission(portfolio) - and self.has_edit_request_portfolio_permission(portfolio), - ["View-only admin", "Domain requestor"], - ), - ( - self.has_view_all_domains_portfolio_permission(portfolio) - and self.has_any_requests_portfolio_permission(portfolio), - ["View-only admin"], - ), - ( - self.has_view_portfolio_permission(portfolio) - and self.has_edit_request_portfolio_permission(portfolio) - and self.has_any_domains_portfolio_permission(portfolio), - ["Domain requestor", "Domain manager"], - ), - ( - self.has_view_portfolio_permission(portfolio) and self.has_edit_request_portfolio_permission(portfolio), - ["Domain requestor"], - ), - ( - self.has_view_portfolio_permission(portfolio) and self.has_any_domains_portfolio_permission(portfolio), - ["Domain manager"], - ), - (self.has_view_portfolio_permission(portfolio), ["Member"]), - ] - - # Evaluate conditions and add roles - for condition, role_list in conditions_roles: - if condition: - roles.extend(role_list) - break - - return roles - def get_portfolios(self): return self.portfolio_permissions.all() diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index d21678d58..c0204b4ca 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -251,13 +251,6 @@ def is_members_subpage(path): return get_url_name(path) in url_names -@register.filter(name="portfolio_role_summary") -def portfolio_role_summary(user, portfolio): - """Returns the value of user.portfolio_role_summary""" - if user and portfolio: - return user.portfolio_role_summary(portfolio) - else: - return [] @register.filter(name="display_requesting_entity") diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 4401b73e8..cd2fe868d 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1191,67 +1191,6 @@ class TestUser(TestCase): User.objects.all().delete() UserDomainRole.objects.all().delete() - @patch.object(User, "has_edit_portfolio_permission", return_value=True) - def test_portfolio_role_summary_admin(self, mock_edit_org): - # Test if the user is recognized as an Admin - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Admin"]) - - @patch.multiple( - User, - has_view_all_domains_portfolio_permission=lambda self, portfolio: True, - has_any_requests_portfolio_permission=lambda self, portfolio: True, - has_edit_request_portfolio_permission=lambda self, portfolio: True, - ) - def test_portfolio_role_summary_view_only_admin_and_domain_requestor(self): - # Test if the user has both 'View-only admin' and 'Domain requestor' roles - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["View-only admin", "Domain requestor"]) - - @patch.multiple( - User, - has_view_all_domains_portfolio_permission=lambda self, portfolio: True, - has_any_requests_portfolio_permission=lambda self, portfolio: True, - ) - def test_portfolio_role_summary_view_only_admin(self): - # Test if the user is recognized as a View-only admin - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["View-only admin"]) - - @patch.multiple( - User, - has_view_portfolio_permission=lambda self, portfolio: True, - has_edit_request_portfolio_permission=lambda self, portfolio: True, - has_any_domains_portfolio_permission=lambda self, portfolio: True, - ) - def test_portfolio_role_summary_member_domain_requestor_domain_manager(self): - # Test if the user has 'Member', 'Domain requestor', and 'Domain manager' roles - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Domain requestor", "Domain manager"]) - - @patch.multiple( - User, - has_view_portfolio_permission=lambda self, portfolio: True, - has_edit_request_portfolio_permission=lambda self, portfolio: True, - ) - def test_portfolio_role_summary_member_domain_requestor(self): - # Test if the user has 'Member' and 'Domain requestor' roles - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Domain requestor"]) - - @patch.multiple( - User, - has_view_portfolio_permission=lambda self, portfolio: True, - has_any_domains_portfolio_permission=lambda self, portfolio: True, - ) - def test_portfolio_role_summary_member_domain_manager(self): - # Test if the user has 'Member' and 'Domain manager' roles - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Domain manager"]) - - @patch.multiple(User, has_view_portfolio_permission=lambda self, portfolio: True) - def test_portfolio_role_summary_member(self): - # Test if the user is recognized as a Member - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Member"]) - - def test_portfolio_role_summary_empty(self): - # Test if the user has no roles - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), []) - @patch("registrar.models.User._has_portfolio_permission") def test_has_view_portfolio_permission(self, mock_has_permission): mock_has_permission.return_value = True From 70f5a655a8ae9a7b27aa778ecf68d9c1f401d815 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Sun, 16 Feb 2025 19:00:23 -0700 Subject: [PATCH 137/139] linted --- src/registrar/templatetags/custom_filters.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index c0204b4ca..ff73e6dc1 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -251,8 +251,6 @@ def is_members_subpage(path): return get_url_name(path) in url_names - - @register.filter(name="display_requesting_entity") def display_requesting_entity(domain_request): """Workaround for a newline issue in .txt files (our emails) as if statements From 3493b9a16f9056ad4b625ae909f9768b29956557 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:30:30 -0700 Subject: [PATCH 138/139] Less specific selector --- src/registrar/assets/src/js/getgov-admin/analytics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/src/js/getgov-admin/analytics.js b/src/registrar/assets/src/js/getgov-admin/analytics.js index 7524b726f..47bc81388 100644 --- a/src/registrar/assets/src/js/getgov-admin/analytics.js +++ b/src/registrar/assets/src/js/getgov-admin/analytics.js @@ -146,7 +146,7 @@ function createComparativeColumnChart(id, title, labelOne, labelTwo) { /** An IIFE to initialize the analytics page */ export function initAnalyticsDashboard() { - const analyticsPageContainer = document.querySelector('.analytics-dashboard .analytics-dashboard-charts'); + const analyticsPageContainer = document.querySelector('.analytics-dashboard-charts'); if (analyticsPageContainer) { document.addEventListener("DOMContentLoaded", function () { initAnalyticsExportButtons(); From 0f0048fe335582ba9bc8984db2b56980e8073158 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:34:50 -0700 Subject: [PATCH 139/139] fix button wrap --- src/registrar/templates/admin/analytics.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index d345aeb14..fdebff22c 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -95,7 +95,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
    -
      +