Skip to content

Commit

Permalink
Merge branch 'main' into za/1484-domain-manager-delete
Browse files Browse the repository at this point in the history
  • Loading branch information
zandercymatics committed Jan 31, 2024
2 parents 161bd30 + 5d128bb commit e72c6a5
Show file tree
Hide file tree
Showing 34 changed files with 764 additions and 227 deletions.
4 changes: 2 additions & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ All other changes require just a single approving review.-->
- [ ] Added at least 2 developers as PR reviewers (only 1 will need to approve)
- [ ] Messaged on Slack or in standup to notify the team that a PR is ready for review
- [ ] Changes to “how we do things” are documented in READMEs and or onboarding guide
- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the assoicated migrations file has been commited.
- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the associated migrations file has been commited.

#### Ensured code standards are met (Original Developer)

Expand All @@ -72,7 +72,7 @@ All other changes require just a single approving review.-->
- [ ] Reviewed this code and left comments
- [ ] Checked that all code is adequately covered by tests
- [ ] Made it clear which comments need to be addressed before this work is merged
- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the assoicated migrations file has been commited.
- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the associated migrations file has been commited.

#### Ensured code standards are met (Code reviewer)

Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/deploy-development.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,11 @@ jobs:
cf_org: cisa-dotgov
cf_space: development
push_arguments: "-f ops/manifests/manifest-development.yaml"
- name: Run Django migrations
uses: cloud-gov/cg-cli-tools@main
with:
cf_username: ${{ secrets.CF_DEVELOPMENT_USERNAME }}
cf_password: ${{ secrets.CF_DEVELOPMENT_PASSWORD }}
cf_org: cisa-dotgov
cf_space: development
cf_command: "run-task getgov-development --command 'python manage.py migrate' --name migrate"
4 changes: 4 additions & 0 deletions docs/operations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,7 @@ Bugs on production software need to be documented quickly and triaged to determi
3. In the case where the engineering lead is is unresponsive or unavailable to assign the ticket immediately, the product team will make sure an engineer volunteers or is assigned to the ticket/PR review ASAP.
4. Once done, the developer must make a PR and should tag the assigned PR reviewers in our Slack dev channel stating that the PR is now waiting on their review. These reviewers should drop other tasks in order to review this promptly.
5. See the the section above on [Making bug fixes on stable](#making-bug-fixes-on-stable-during-production) for how to push changes to stable once the PR is approved

# Investigating and monitoring the health of the Registrar

Sometimes, we may want individuals to routinely monitor the Registrar's health, such as after big feature launches. The cadence of such monitoring and what we look for is subject to change and is instead documented in [Checklist for production verification document](https://docs.google.com/document/d/15b_qwEZMiL76BHeRHnznV1HxDQcxNRt--vPSEfixBOI). All project team members should feel free to suggest edits to this document and should refer to it if production-level monitoring is underway.
60 changes: 58 additions & 2 deletions src/registrar/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from auditlog.models import LogEntry # type: ignore
from auditlog.admin import LogEntryAdmin # type: ignore
from django_fsm import TransitionNotAllowed # type: ignore
from django.utils.safestring import mark_safe
from django.utils.html import escape

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -452,6 +454,60 @@ def get_readonly_fields(self, request, obj=None):
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields # Read-only fields for analysts

def change_view(self, request, object_id, form_url="", extra_context=None):
"""Extend the change_view for Contact objects in django admin.
Customize to display related objects to the Contact. These will be passed
through the messages construct to the template for display to the user."""

# Fetch the Contact instance
contact = models.Contact.objects.get(pk=object_id)

# initialize related_objects array
related_objects = []
# for all defined fields in the model
for related_field in contact._meta.get_fields():
# if the field is a relation to another object
if related_field.is_relation:
# Check if the related field is not None
related_manager = getattr(contact, related_field.name)
if related_manager is not None:
# Check if it's a ManyToManyField/reverse ForeignKey or a OneToOneField
# Do this by checking for get_queryset method on the related_manager
if hasattr(related_manager, "get_queryset"):
# Handles ManyToManyRel and ManyToOneRel
queryset = related_manager.get_queryset()
else:
# Handles OneToOne rels, ie. User
queryset = [related_manager]

for obj in queryset:
# for each object, build the edit url in this view and add as tuple
# to the related_objects array
app_label = obj._meta.app_label
model_name = obj._meta.model_name
obj_id = obj.id
change_url = reverse("admin:%s_%s_change" % (app_label, model_name), args=[obj_id])
related_objects.append((change_url, obj))

if related_objects:
message = "<ul class='messagelist_content-list--unstyled'>"
for i, (url, obj) in enumerate(related_objects):
if i < 5:
escaped_obj = escape(obj)
message += f"<li>Joined to {obj.__class__.__name__}: <a href='{url}'>{escaped_obj}</a></li>"
message += "</ul>"
if len(related_objects) > 5:
related_objects_over_five = len(related_objects) - 5
message += f"<p class='font-sans-3xs'>And {related_objects_over_five} more...</p>"

message_html = mark_safe(message) # nosec
messages.warning(
request,
message_html,
)

return super().change_view(request, object_id, form_url, extra_context=extra_context)


class WebsiteAdmin(ListHeaderAdmin):
"""Custom website admin class."""
Expand Down Expand Up @@ -1239,7 +1295,7 @@ class DraftDomainAdmin(ListHeaderAdmin):
search_help_text = "Search by draft domain name."


class VeryImportantPersonAdmin(ListHeaderAdmin):
class VerifiedByStaffAdmin(ListHeaderAdmin):
list_display = ("email", "requestor", "truncated_notes", "created_at")
search_fields = ["email"]
search_help_text = "Search by email."
Expand Down Expand Up @@ -1282,4 +1338,4 @@ def save_model(self, request, obj, form, change):
admin.site.register(models.PublicContact, AuditedAdmin)
admin.site.register(models.DomainApplication, DomainApplicationAdmin)
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
admin.site.register(models.VeryImportantPerson, VeryImportantPersonAdmin)
admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
97 changes: 86 additions & 11 deletions src/registrar/assets/js/get-gov.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function inlineToast(el, id, style, msg) {
}
}

function _checkDomainAvailability(el) {
function checkDomainAvailability(el) {
const callback = (response) => {
toggleInputValidity(el, (response && response.available), msg=response.message);
announce(el.id, response.message);
Expand All @@ -154,9 +154,6 @@ function _checkDomainAvailability(el) {
fetchJSON(`available/?domain=${el.value}`, callback);
}

/** Call the API to see if the domain is good. */
const checkDomainAvailability = debounce(_checkDomainAvailability);

/** Hides the toast message and clears the aira live region. */
function clearDomainAvailability(el) {
el.classList.remove('usa-input--success');
Expand Down Expand Up @@ -206,13 +203,33 @@ function handleInputValidation(e) {
}

/** On button click, handles running any associated validators. */
function handleValidationClick(e) {
function validateFieldInput(e) {
const attribute = e.target.getAttribute("validate-for") || "";
if (!attribute.length) return;
const input = document.getElementById(attribute);
removeFormErrors(input, true);
runValidators(input);
}


function validateFormsetInputs(e, availabilityButton) {

// Collect input IDs from the repeatable forms
let inputs = Array.from(document.querySelectorAll('.repeatable-form input'))

// Run validators for each input
inputs.forEach(input => {
removeFormErrors(input, true);
runValidators(input);
});

// Set the validate-for attribute on the button with the collected input IDs
// Not needed for functionality but nice for accessibility
inputs = inputs.map(input => input.id).join(', ');
availabilityButton.setAttribute('validate-for', inputs);

}

// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
// Initialization code.

Expand All @@ -232,14 +249,64 @@ function handleValidationClick(e) {
for(const input of needsValidation) {
input.addEventListener('input', handleInputValidation);
}
const alternativeDomainsAvailability = document.getElementById('validate-alt-domains-availability');
const activatesValidation = document.querySelectorAll('[validate-for]');

for(const button of activatesValidation) {
button.addEventListener('click', handleValidationClick);
// Adds multi-field validation for alternative domains
if (button === alternativeDomainsAvailability) {
button.addEventListener('click', (e) => {
validateFormsetInputs(e, alternativeDomainsAvailability)
});
} else {
button.addEventListener('click', validateFieldInput);
}
}
})();

/**
* Delete method for formsets that diff in the view and delete in the model (Nameservers, DS Data)
* Removes form errors surrounding a form input
*/
function removeFormErrors(input, removeStaleAlerts=false){
// Remove error message
let errorMessage = document.getElementById(`${input.id}__error-message`);
if (errorMessage) {
errorMessage.remove();
}else{
return
}

// Remove error classes
if (input.classList.contains('usa-input--error')) {
input.classList.remove('usa-input--error');
}

// Get the form label
let label = document.querySelector(`label[for="${input.id}"]`);
if (label) {
label.classList.remove('usa-label--error');

// Remove error classes from parent div
let parentDiv = label.parentElement;
if (parentDiv) {
parentDiv.classList.remove('usa-form-group--error');
}
}

if (removeStaleAlerts){
let staleAlerts = document.querySelectorAll(".usa-alert--error")
for (let alert of staleAlerts){
// Don't remove the error associated with the input
if (alert.id !== `${input.id}--toast`) {
alert.remove()
}
}
}
}

/**
* Prepare the namerservers and DS data forms delete buttons
* We will call this on the forms init, and also every time we add a form
*
*/
function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){
Expand Down Expand Up @@ -460,6 +527,7 @@ function hideDeletedForms() {
let isNameserversForm = document.querySelector(".nameservers-form");
let isOtherContactsForm = document.querySelector(".other-contacts-form");
let isDsDataForm = document.querySelector(".ds-data-form");
let isDotgovDomain = document.querySelector(".dotgov-domain-form");
// The Nameservers formset features 2 required and 11 optionals
if (isNameserversForm) {
cloneIndex = 2;
Expand All @@ -472,6 +540,8 @@ function hideDeletedForms() {
formLabel = "Organization contact";
container = document.querySelector("#other-employees");
formIdentifier = "other_contacts"
} else if (isDotgovDomain) {
formIdentifier = "dotgov_domain"
}
let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`);

Expand Down Expand Up @@ -554,6 +624,7 @@ function hideDeletedForms() {
// Reset the values of each input to blank
inputs.forEach((input) => {
input.classList.remove("usa-input--error");
input.classList.remove("usa-input--success");
if (input.type === "text" || input.type === "number" || input.type === "password" || input.type === "email" || input.type === "tel") {
input.value = ""; // Set the value to an empty string

Expand All @@ -566,22 +637,25 @@ function hideDeletedForms() {
let selects = newForm.querySelectorAll("select");
selects.forEach((select) => {
select.classList.remove("usa-input--error");
select.classList.remove("usa-input--success");
select.selectedIndex = 0; // Set the value to an empty string
});

let labels = newForm.querySelectorAll("label");
labels.forEach((label) => {
label.classList.remove("usa-label--error");
label.classList.remove("usa-label--success");
});

let usaFormGroups = newForm.querySelectorAll(".usa-form-group");
usaFormGroups.forEach((usaFormGroup) => {
usaFormGroup.classList.remove("usa-form-group--error");
usaFormGroup.classList.remove("usa-form-group--success");
});

// Remove any existing error messages
let usaErrorMessages = newForm.querySelectorAll(".usa-error-message");
usaErrorMessages.forEach((usaErrorMessage) => {
// Remove any existing error and success messages
let usaMessages = newForm.querySelectorAll(".usa-error-message, .usa-alert");
usaMessages.forEach((usaErrorMessage) => {
let parentDiv = usaErrorMessage.closest('div');
if (parentDiv) {
parentDiv.remove(); // Remove the parent div if it exists
Expand All @@ -592,7 +666,8 @@ function hideDeletedForms() {

// Attach click event listener on the delete buttons of the new form
let newDeleteButton = newForm.querySelector(".delete-record");
prepareNewDeleteButton(newDeleteButton, formLabel);
if (newDeleteButton)
prepareNewDeleteButton(newDeleteButton, formLabel);

// Disable the add more button if we have 13 forms
if (isNameserversForm && formNum == 13) {
Expand Down
14 changes: 13 additions & 1 deletion src/registrar/assets/sass/_theme/_admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ h1, h2, h3,

.module h3 {
padding: 0;
color: var(--primary);
color: var(--link-fg);
margin: units(2) 0 units(1) 0;
}

Expand Down Expand Up @@ -258,3 +258,15 @@ h1, h2, h3,
#select2-id_user-results {
width: 100%;
}

// Content list inside of a DjA alert, unstyled
.messagelist_content-list--unstyled {
padding-left: 0;
li {
font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif;
font-size: 13.92px!important;
background: none!important;
padding: 0!important;
margin: 0!important;
}
}
14 changes: 14 additions & 0 deletions src/registrar/assets/sass/_theme/_lists.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@use "uswds-core" as *;

dt {
color: color('primary-dark');
margin-top: units(2);
font-weight: font-weight('semibold');
// The units mixin can only get us close, so it's between
// hardcoding the value and using in markup
font-size: 16.96px;
}
dd {
margin-left: 0;
}

14 changes: 13 additions & 1 deletion src/registrar/assets/sass/_theme/_register-form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
margin-top: units(-1);
}

//Tighter spacing when H2 is immediatly after H1
// Tighter spacing when h2 is immediatly after h1
.register-form-step .usa-fieldset:first-of-type h2:first-of-type,
.register-form-step h1 + h2 {
margin-top: units(1);
}

// register-form-review-header is used on the summary page and
// should not be styled like the register form headers
.register-form-step h3 {
color: color('primary-dark');
letter-spacing: $letter-space--xs;
Expand All @@ -23,6 +25,16 @@
}
}

h3.register-form-review-header {
color: color('primary-dark');
margin-top: units(2);
margin-bottom: 0;
font-weight: font-weight('semibold');
// The units mixin can only get us close, so it's between
// hardcoding the value and using in markup
font-size: 16.96px;
}

.register-form-step h4 {
margin-bottom: 0;

Expand Down
1 change: 1 addition & 0 deletions src/registrar/assets/sass/_theme/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
--- Custom Styles ---------------------------------*/
@forward "base";
@forward "typography";
@forward "lists";
@forward "buttons";
@forward "forms";
@forward "fieldsets";
Expand Down
2 changes: 1 addition & 1 deletion src/registrar/forms/application_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ def clean_alternative_domain(self):

alternative_domain = forms.CharField(
required=False,
label="",
label="Alternative domain",
)


Expand Down
Loading

0 comments on commit e72c6a5

Please sign in to comment.