Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Not on a sandbox) Ticket #1501: Delete button for DomainApplication records #1590

Merged
merged 73 commits into from
Jan 23, 2024

Conversation

zandercymatics
Copy link
Contributor

@zandercymatics zandercymatics commented Jan 4, 2024

Ticket

Resolves #1501

Changes

  • Added some styles in _tables.scss for positioning, increased width of card on the home page
  • Modified home.html to include the delete button
  • Added unit tests in test_views.py
  • Added a new delete view in application.py for DomainApplication w/ a has_permission override to forbid certain domains from being deleted
  • Modified index.py to add a context for the modal button and if there are any applications which can be deleted
  • Added an abstract permission view for DomainApplication
  • Adds some logic for displaying multiple unfinished applications

Context for reviewers

This ticket adds a delete button for DomainApplications on the home page, and some logic where when you start multiple new domain applications, they will be numbered rather than having duplicate names.

Regarding the delete button, it is only visible if the domain is in the state of started or withdrawn, otherwise it cannot be deleted.

Setup

For designers
Everything you need should already exists on my sandbox - one domainapplication in STARTED and one in WITHDRAWN. If this is not the case (i.e. there is not delete button for any of the DomainApplications), then you will need to navigate to /admin and add yourself as a submitter to a DomainApplication with either of those states.

Afterwards, just follow the workflow defined in the figma.

For developers
IMPORTANT: Since this PR adds a scss file and makes some of those changes, you will likely need to do collectstatic if that local cache is not refreshing for you.

This PR should be fairly simple to review. When testing locally, all you need to do is boot up the application and you will have an assortment to test with.

Afterwards, start two domain applications, but do not finish either of them. Note that they will be numbered...

On sandboxes, you should already have this, but if you do not - grab a few applications and set yourself as a submitter. Then, delete them.

Code Review Verification Steps

As the original developer, I have

Satisfied acceptance criteria and met development standards

  • Met the acceptance criteria, or will meet them in a subsequent PR
  • Created/modified automated tests
  • 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.

Ensured code standards are met (Original Developer)

  • All new functions and methods are commented using plain language
  • Did dependency updates in Pipfile also get changed in requirements.txt?
  • Interactions with external systems are wrapped in try/except
  • Error handling exists for unusual or missing values

Validated user-facing changes (if applicable)

  • New pages have been added to .pa11yci file so that they will be tested with our automated accessibility testing
  • Checked keyboard navigability
  • Tested general usability, landmarks, page header structure, and links with a screen reader (such as Voiceover or ANDI)
  • Add at least 1 designer as PR reviewer

As a code reviewer, I have

Reviewed, tested, and left feedback about the changes

  • Pulled this branch locally and tested it
  • 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.

Ensured code standards are met (Code reviewer)

  • All new functions and methods are commented using plain language
  • Interactions with external systems are wrapped in try/except
  • Error handling exists for unusual or missing values
  • (Rarely needed) Did dependency updates in Pipfile also get changed in requirements.txt?

Validated user-facing changes as a developer

  • New pages have been added to .pa11yci file so that they will be tested with our automated accessibility testing

  • Checked keyboard navigability

  • Meets all designs and user flows provided by design/product

  • Tested general usability, landmarks, page header structure, and links with a screen reader (such as Voiceover or ANDI)

  • Tested with multiple browsers, the suggestion is to use ones that the developer didn't (check off which ones were used)

    • Chrome
    • Microsoft Edge
    • FireFox
    • Safari
  • (Rarely needed) Tested as both an analyst and applicant user

Note: Multiple code reviewers can share the checklists above, a second reviewers should not make a duplicate checklist

As a designer reviewer, I have

Verified that the changes match the design intention

  • Checked that the design translated visually
  • Checked behavior
  • Checked different states (empty, one, some, error)
  • Checked for landmarks, page heading structure, and links
  • Tried to break the intended flow

Validated user-facing changes as a designer

  • Checked keyboard navigability

  • Tested general usability, landmarks, page header structure, and links with a screen reader (such as Voiceover or ANDI)

  • Tested with multiple browsers (check off which ones were used)

    • Chrome
    • Microsoft Edge
    • FireFox
    • Safari
  • (Rarely needed) Tested as both an analyst and applicant user

Screenshots

image

@zandercymatics zandercymatics marked this pull request as draft January 4, 2024 21:35
Copy link

github-actions bot commented Jan 4, 2024

🥳 Successfully deployed to developer sandbox za.

Copy link

github-actions bot commented Jan 5, 2024

🥳 Successfully deployed to developer sandbox za.

1 similar comment
Copy link

github-actions bot commented Jan 5, 2024

🥳 Successfully deployed to developer sandbox za.

Copy link

github-actions bot commented Jan 5, 2024

🥳 Successfully deployed to developer sandbox za.

Copy link

github-actions bot commented Jan 5, 2024

🥳 Successfully deployed to developer sandbox za.

Copy link

github-actions bot commented Jan 5, 2024

🥳 Successfully deployed to developer sandbox za.

@zandercymatics zandercymatics marked this pull request as ready for review January 5, 2024 19:57
Copy link

github-actions bot commented Jan 5, 2024

🥳 Successfully deployed to developer sandbox za.

Copy link

github-actions bot commented Jan 5, 2024

🥳 Successfully deployed to developer sandbox za.

Comment on lines 9 to 13
.usa-modal {
.usa-modal__heading.blue-header{
color: $dhs-blue;
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same color as the figma but its not consistent with the other modals in our app (consistency difference between that and the figma). Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zandercymatics sometimes I forget to update the color when pulling in the USWDS base modal component because the color is so similar to our .gov palette. Go ahead and match the color we have for our styles on this and any modals going forward.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double checked and I sometimes mistakenly use medium-50 when it should be dark-60. My bad!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a problem!

Comment on lines +148 to +159
# Given that we are including a subset of items that can be deleted while excluding the rest,
# subTest is appropriate here as otherwise we would need many duplicate tests for the same reason.
draft_domain = DraftDomain.objects.create(name="igorville.gov")
for status in DomainApplication.ApplicationStatus:
if status not in [
DomainApplication.ApplicationStatus.STARTED,
DomainApplication.ApplicationStatus.WITHDRAWN,
]:
with self.subTest(status=status):
application = DomainApplication.objects.create(
creator=self.user, requested_domain=draft_domain, status=status
)
Copy link
Contributor Author

@zandercymatics zandercymatics Jan 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be re-implemented to exclude the for loop but it doesn't make sense to do here in this context because it would make the test brittle. This is based off of other unit tests using subTest which test on status as well (FSM checks)

@zandercymatics zandercymatics changed the title (Draft) Ticket #1501: Delete button for DomainApplication records (On getgov-za) Ticket #1501: Delete button for DomainApplication records Jan 5, 2024
Copy link
Contributor

@allly-b allly-b left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for approving before, I forgot to check that contacts were being deleted. That needs to happen here so we don't create orphaned contacts on our production environment.

Copy link

🥳 Successfully deployed to developer sandbox za.

Copy link

🥳 Successfully deployed to developer sandbox za.

Copy link
Contributor

@gabo0oo gabo0oo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

Copy link

🥳 Successfully deployed to developer sandbox za.

Copy link

🥳 Successfully deployed to developer sandbox za.

Copy link

🥳 Successfully deployed to developer sandbox za.

Copy link

🥳 Successfully deployed to developer sandbox za.

Comment on lines 646 to 664
def post(self, request, *args, **kwargs):
# Grab all orphaned contacts
application: DomainApplication = self.get_object()
contacts_to_delete, duplicates = self._get_orphaned_contacts(application)

# Delete the DomainApplication
response = super().post(request, *args, **kwargs)

# Delete orphaned contacts - but only for if they are not associated with a user
Contact.objects.filter(id__in=contacts_to_delete, user=None).delete()

# After a delete occurs, do a second sweep on any returned duplicates.
# This determines if any of these three fields share a contact, which is used for
# the edge case where the same user may be an AO, and a submitter, for example.
if len(duplicates) > 0:
duplicates_to_delete, _ = self._get_orphaned_contacts(application)
Contact.objects.filter(id__in=duplicates_to_delete, user=None).delete()

return response
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is being done at the form level, as the ticket which handled the joins was done at this level too. From my understanding, we are trying to avoid modifying model-level behavior where possible, and this could be labelled as falling under that triage.

For a future ticket (probably during some sort of refactor or reconciliation), I would recommend some sort of helper, utility, class, etc that handles orphaned records more generically. This can be done either at the model or form level, but that on its own is another beast (metaprogramming) - but it can be done. I do not think this should be done now, though. Only if we have a need for it

Comment on lines 676 to 684
# Check if the desired item still exists in the DB
if check_db:
ao = self._get_contacts_by_id([ao.id]).first() if ao is not None else None
submitter = self._get_contacts_by_id([submitter.id]).first() if submitter is not None else None
other_contacts = self._get_contacts_by_id(other_contact_ids)

# Pair each contact with its related name
checked_contacts = [(ao, "authorizing_official"), (submitter, "submitted_applications")]
checked_contacts.extend((contact, "contact_applications") for contact in other_contacts)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not the most elegant thing ever but it works. Let me know if this needs changing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed but makes sense here, just would update the comment to make it easier to read why this is happening

@zandercymatics zandercymatics dismissed allly-b’s stale review January 22, 2024 18:22

Implemented changes

Copy link
Contributor

@allly-b allly-b left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks for the change. A couple minor suggestions and for sure remove the availability check change before merging. That really should be linked to the associated ticket in it's own PR


# Create the site and contacts to delete (orphaned)
contact = Contact.objects.create(
first_name="Henry",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

total tangent It would have been funny if this contact was named Oliver Twist or Bruce Wayne, since it gets orphaned.

@@ -57,6 +57,9 @@ def _validate_domain_string(domain, blank_ok):
# If blank ok is true, just return the domain
return domain

if domain.startswith("www."):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking, fix before merge This is part of another ticket and shouldn't be combined into this PR

other_contact_ids = application.other_contacts.all().values_list("id", flat=True)

# Check if the desired item still exists in the DB
if check_db:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: are you saying here that you can have a foreign key value filled in with an id and then have it not exist in the actual foreign table (contact). That shouldn't be possible if things were set up right. Was this check done just to be cautious or because you saw this happen

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(More info below) basically, there is a scenario where it is possible to have dangling contacts even after a delete. This occurs when the contact is shared among two contact fields (like AO and submitter). From the join functions perspective, it looks fine. But from our perspective (as long as its not shared anywhere else), we know that it will still be orphaned after its deleted. After we determine (with duplicates) that we ran into this edge case, we do the same join check, but check by getting updated values from the db. If it still exists in the DB and its an orphan, we caught that edge case

Comment on lines 676 to 684
# Check if the desired item still exists in the DB
if check_db:
ao = self._get_contacts_by_id([ao.id]).first() if ao is not None else None
submitter = self._get_contacts_by_id([submitter.id]).first() if submitter is not None else None
other_contacts = self._get_contacts_by_id(other_contact_ids)

# Pair each contact with its related name
checked_contacts = [(ao, "authorizing_official"), (submitter, "submitted_applications")]
checked_contacts.extend((contact, "contact_applications") for contact in other_contacts)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed but makes sense here, just would update the comment to make it easier to read why this is happening

return response

def _get_orphaned_contacts(self, application: DomainApplication, check_db=True):
"""Collects all orphaned contacts"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change before merging: add info on what this returns since it's not initially clear

return contacts

def _get_duplicates(self, objects):
"""Given a list of objects, return a list of which items were duplicates"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix before merging: this is only ever used for contacts, I would be more specific here this is for contacts, especially since you use the variable "contact" below. Also, why not just convert objects to a set and then back to a list? That will automatically remove duplicates in a list normally, though I have never tried with a query result so curious if you ran into an issue there.

Copy link
Contributor Author

@zandercymatics zandercymatics Jan 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason why I am not converting it to a set is because I actually want to get a list of duplicates, not remove them. This is because duplicates let me detect an edge case:

A contact (which is not joined on any other field) is attached to both the authorizing_official field and the submitter field. When the delete is ran, it "looks" like this field is not an orphan (because it technically isn't) - but that isn't correct, because it will be after the delete.

This is basically a check to see if we run into that case, i.e. if the contact is shared among any of the fields on Application. If it is, there is a good chance that we hit this edge case - so we use this as a flag to determine if we should do a second join check or not (which will occur after delete, so it will now pick up if that contact became orphaned from the delete)

zandercymatics and others added 2 commits January 23, 2024 08:18
Co-authored-by: Alysia Broddrick <109625347+abroddrick@users.noreply.github.com>
Co-authored-by: Alysia Broddrick <109625347+abroddrick@users.noreply.github.com>
Copy link

🥳 Successfully deployed to developer sandbox za.

Copy link

🥳 Successfully deployed to developer sandbox za.

@zandercymatics zandercymatics merged commit 2d13f37 into main Jan 23, 2024
9 checks passed
@zandercymatics zandercymatics deleted the za/1501-users-delete-domain-records branch January 23, 2024 15:48
Copy link

🥳 Successfully deployed to developer sandbox za.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow users to delete domain requests they've started or withdrawn
6 participants