From 77ddc6700aef46fd224c07cf6bbf260c332f8d81 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 4 Feb 2025 14:34:00 -0800 Subject: [PATCH 01/36] 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 4b523a75ceab37fe36c5e117871ab722c1cef749 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 4 Feb 2025 16:13:07 -0800 Subject: [PATCH 02/36] 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 f2d14ad715fb36bdbdab751099ca8359edf8a7e1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 6 Feb 2025 10:08:04 -0700 Subject: [PATCH 03/36] Try adding patternomaly --- src/package-lock.json | 7 +++++++ src/package.json | 1 + 2 files changed, 8 insertions(+) diff --git a/src/package-lock.json b/src/package-lock.json index 5caff976c..324e5a955 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -18,6 +18,7 @@ "@babel/preset-env": "^7.26.0", "@uswds/compile": "1.2.1", "babel-loader": "^9.2.1", + "patternomaly": "^1.3.2", "sass-loader": "^12.6.0", "webpack": "^5.96.1", "webpack-stream": "^7.0.0" @@ -5362,6 +5363,12 @@ "node": ">=8" } }, + "node_modules/patternomaly": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/patternomaly/-/patternomaly-1.3.2.tgz", + "integrity": "sha512-70UhA5+ZrnNgdfDBKXIGbMHpP+naTzfx9vPT4KwIdhtWWs0x6FWZRJQMXXhV2jcK0mxl28FA/2LPAKArNG058Q==", + "dev": true + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", diff --git a/src/package.json b/src/package.json index d915b2384..87602bde9 100644 --- a/src/package.json +++ b/src/package.json @@ -19,6 +19,7 @@ "@babel/preset-env": "^7.26.0", "@uswds/compile": "1.2.1", "babel-loader": "^9.2.1", + "patternomaly": "^1.3.2", "sass-loader": "^12.6.0", "webpack": "^5.96.1", "webpack-stream": "^7.0.0" From 039882b9187ce797ae817114d111a4f17c911f16 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:02:21 -0700 Subject: [PATCH 04/36] add patternomaly --- src/registrar/assets/js/get-gov-reports.js | 7 +++++++ src/registrar/templates/admin/base_site.html | 1 + 2 files changed, 8 insertions(+) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index 92bba4a1f..a77e1244b 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -1,3 +1,4 @@ + /** 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. @@ -78,6 +79,9 @@ borderColor: "rgba(255, 99, 132, 1)", borderWidth: 1, data: listOne, + backgroundColor: [ + pattern.draw('square', '#1f77b4'), + ] }, { label: labelTwo, @@ -85,6 +89,9 @@ borderColor: "rgba(75, 192, 192, 1)", borderWidth: 1, data: listTwo, + backgroundColor: [ + pattern.draw('square', '#1f77b4'), + ] }, ], }; diff --git a/src/registrar/templates/admin/base_site.html b/src/registrar/templates/admin/base_site.html index b80917bb2..d7c43647a 100644 --- a/src/registrar/templates/admin/base_site.html +++ b/src/registrar/templates/admin/base_site.html @@ -23,6 +23,7 @@ + {% endblock %} From 90784eb69eacfe71d0ae6069e95ef21526c57f76 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:25:46 -0700 Subject: [PATCH 05/36] Change pattern --- src/registrar/assets/js/get-gov-reports.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index a77e1244b..8bfe32fdd 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -80,7 +80,7 @@ borderWidth: 1, data: listOne, backgroundColor: [ - pattern.draw('square', '#1f77b4'), + pattern.draw('zigzag-vertical', '#1f77b4'), ] }, { @@ -90,7 +90,7 @@ borderWidth: 1, data: listTwo, backgroundColor: [ - pattern.draw('square', '#1f77b4'), + pattern.draw('diagonal', '#1f77b4'), ] }, ], From 3322837f0d289f69edc18563e4a0515db30f6f73 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:34:24 -0700 Subject: [PATCH 06/36] revert node changes --- src/package-lock.json | 7 ------- src/package.json | 1 - 2 files changed, 8 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 324e5a955..5caff976c 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -18,7 +18,6 @@ "@babel/preset-env": "^7.26.0", "@uswds/compile": "1.2.1", "babel-loader": "^9.2.1", - "patternomaly": "^1.3.2", "sass-loader": "^12.6.0", "webpack": "^5.96.1", "webpack-stream": "^7.0.0" @@ -5363,12 +5362,6 @@ "node": ">=8" } }, - "node_modules/patternomaly": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/patternomaly/-/patternomaly-1.3.2.tgz", - "integrity": "sha512-70UhA5+ZrnNgdfDBKXIGbMHpP+naTzfx9vPT4KwIdhtWWs0x6FWZRJQMXXhV2jcK0mxl28FA/2LPAKArNG058Q==", - "dev": true - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", diff --git a/src/package.json b/src/package.json index 87602bde9..d915b2384 100644 --- a/src/package.json +++ b/src/package.json @@ -19,7 +19,6 @@ "@babel/preset-env": "^7.26.0", "@uswds/compile": "1.2.1", "babel-loader": "^9.2.1", - "patternomaly": "^1.3.2", "sass-loader": "^12.6.0", "webpack": "^5.96.1", "webpack-stream": "^7.0.0" From f60ed6c0eaae9e78888375f5736cc7baf55437a7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:40:55 -0700 Subject: [PATCH 07/36] Create patternomaly.js --- src/registrar/assets/js/patternomaly.js | 1110 +++++++++++++++++++++++ 1 file changed, 1110 insertions(+) create mode 100644 src/registrar/assets/js/patternomaly.js diff --git a/src/registrar/assets/js/patternomaly.js b/src/registrar/assets/js/patternomaly.js new file mode 100644 index 000000000..e349db0f0 --- /dev/null +++ b/src/registrar/assets/js/patternomaly.js @@ -0,0 +1,1110 @@ +// 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 From 95d912ecbcdb464a2231995cd17cfecd76991f02 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 6 Feb 2025 13:04:53 -0800 Subject: [PATCH 08/36] Did it the endpoint method way which was sadly incorrect --- src/registrar/admin.py | 48 ++++++--------- .../assets/src/sass/_theme/_admin.scss | 16 +++++ .../assets/src/sass/_theme/_base.scss | 1 - src/registrar/models/domain_invitation.py | 4 -- .../admin/change_form_object_tools.html | 20 +++++- .../admin/status_with_clipboard.html | 22 ------- .../includes/email_clipboard_fieldset.html | 2 - src/registrar/tests/test_admin_domain.py | 61 +++++++++++++++++++ 8 files changed, 114 insertions(+), 60 deletions(-) delete mode 100644 src/registrar/templates/admin/status_with_clipboard.html diff --git a/src/registrar/admin.py b/src/registrar/admin.py index feb3b1883..b1b3d8adb 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -11,6 +11,7 @@ from django.db.models import ( Value, When, ) + from django.db.models.functions import Concat, Coalesce from django.http import HttpResponseRedirect from registrar.models.federal_agency import FederalAgency @@ -24,7 +25,7 @@ from registrar.utility.admin_helpers import ( from django.conf import settings from django.contrib.messages import get_messages from django.contrib.admin.helpers import AdminForm -from django.shortcuts import redirect +from django.shortcuts import redirect, get_object_or_404 from django_fsm import get_available_FIELD_transitions, FSMField from registrar.models import DomainInformation, Portfolio, UserPortfolioPermission, DomainInvitation from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices @@ -1478,26 +1479,6 @@ 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.""" @@ -1527,18 +1508,29 @@ 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. - # readonly_fields = ["status"] - - readonly_fields = [] + # 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"] autocomplete_fields = ["domain"] 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""" + + if extra_context is None: + extra_context = {} + + # Get the domain invitation object + invitation = get_object_or_404(DomainInvitation, id=object_id) + extra_context["invitation"] = invitation + + return super().change_view(request, object_id, form_url, extra_context) + def save_model(self, request, obj, form, change): """ Override the save_model method. diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index 4f75fd2fb..64b979f2b 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -498,6 +498,22 @@ input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-too font-size: 13px; } +.object-tools li button { + font-family: Source Sans Pro Web, Helvetica Neue, Helvetica, Roboto, Arial, sans-serif; + text-transform: none !important; + font-size: 14px !important; + display: block; + float: left; + padding: 3px 12px; + background: var(--object-tools-bg) !important; + color: var(--object-tools-fg); + font-weight: 400; + font-size: 0.6875rem; + text-transform: uppercase; + letter-spacing: 0.5px; + border-radius: 15px; +} + .module--custom { a { font-size: 13px; diff --git a/src/registrar/assets/src/sass/_theme/_base.scss b/src/registrar/assets/src/sass/_theme/_base.scss index be3b89baf..4f00f4b79 100644 --- a/src/registrar/assets/src/sass/_theme/_base.scss +++ b/src/registrar/assets/src/sass/_theme/_base.scss @@ -46,7 +46,6 @@ body { background-color: color('gray-1'); } - .section-outlined { background-color: color('white'); border: 1px solid color('base-lighter'); diff --git a/src/registrar/models/domain_invitation.py b/src/registrar/models/domain_invitation.py index 3954dea7e..28089dcb5 100644 --- a/src/registrar/models/domain_invitation.py +++ b/src/registrar/models/domain_invitation.py @@ -78,10 +78,6 @@ 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) diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 66011a3c4..541f4d162 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -15,13 +15,28 @@ {% else %} {% endif %} -{% endblock %} - +{% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/admin/status_with_clipboard.html b/src/registrar/templates/admin/status_with_clipboard.html deleted file mode 100644 index a62ca5055..000000000 --- a/src/registrar/templates/admin/status_with_clipboard.html +++ /dev/null @@ -1,22 +0,0 @@ -{% load static %} - -
- {{ field.value | capfirst }} - - - -
- 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 4c0e63d66..7907b5180 100644 --- a/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html +++ b/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html @@ -7,8 +7,6 @@ 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 %} diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py index 867bf1b82..07940e202 100644 --- a/src/registrar/tests/test_admin_domain.py +++ b/src/registrar/tests/test_admin_domain.py @@ -12,6 +12,7 @@ from registrar.models import ( Domain, DomainRequest, DomainInformation, + DomainInvitation, User, Host, Portfolio, @@ -30,6 +31,9 @@ from .common import ( ) from unittest.mock import ANY, call, patch +from django.contrib.messages import get_messages + + import boto3_mocking # type: ignore import logging @@ -495,6 +499,63 @@ class TestDomainInformationInline(MockEppLib): self.assertIn("poopy@gov.gov", domain_managers) +class DomainInvitationAdminTest(TestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.staffuser = create_user(email="staffdomainmanager@meoward.com", is_staff=True) + cls.site = AdminSite() + cls.admin = DomainAdmin(model=Domain, admin_site=cls.site) + cls.factory = RequestFactory() + + def setUp(self): + self.client = Client(HTTP_HOST="localhost:8080") + self.client.force_login(self.staffuser) + super().setUp() + + def test_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 + domain = Domain.objects.create(name="cancelinvitationflowviaadmin.gov") + UserDomainRole.objects.create(user=self.staffuser, domain=domain, role="manager") + + # 2. Invite a domain manager to the above domain + invitation = DomainInvitation.objects.create( + email="inviteddomainmanager@meoward.com", + domain=domain, + status=DomainInvitation.DomainInvitationStatus.INVITED, + ) + + # 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 can 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.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) + + # 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) + + # 7. Get the messages + messages = list(get_messages(response.wsgi_request)) + message_texts = [str(message) for message in messages] + + # 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) + + class TestDomainAdminWithClient(TestCase): """Test DomainAdmin class as super user. 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 09/36] 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 10/36] 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 11/36] 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 12/36] 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 13/36] 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 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 14/36] 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 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 15/36] 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 16/36] 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 17/36] 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 bb05f6f4657c1d6aaab06e3658b128ee86f744a8 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 11 Feb 2025 12:50:40 -0800 Subject: [PATCH 18/36] 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 19/36] 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 20/36] 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 21/36] 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 c540a324c7df33c7011fcef4bebe158a43bf417a Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 12 Feb 2025 10:11:25 -0800 Subject: [PATCH 22/36] 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 0725ab8e59e59ac63267fe1d008998d7fd7ba26c Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 12 Feb 2025 10:31:53 -0800 Subject: [PATCH 23/36] 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 24/36] 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 25/36] 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 26/36] 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 27/36] 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 28/36] 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 29/36] 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 30/36] 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 31/36] 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 32/36] 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 8020bd6f7461b71fa663fc5fff8c4a797eeed907 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 14 Feb 2025 08:02:47 -0800 Subject: [PATCH 33/36] 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 34/36] 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 7d8249923e88a0fa4571d6290e5409d792bb42eb Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 14 Feb 2025 09:17:28 -0800 Subject: [PATCH 35/36] 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 36/36] 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",