-
Notifications
You must be signed in to change notification settings - Fork 23
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
Conversation
🥳 Successfully deployed to developer sandbox za. |
🥳 Successfully deployed to developer sandbox za. |
1 similar comment
🥳 Successfully deployed to developer sandbox za. |
🥳 Successfully deployed to developer sandbox za. |
🥳 Successfully deployed to developer sandbox za. |
🥳 Successfully deployed to developer sandbox za. |
🥳 Successfully deployed to developer sandbox za. |
🥳 Successfully deployed to developer sandbox za. |
.usa-modal { | ||
.usa-modal__heading.blue-header{ | ||
color: $dhs-blue; | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a problem!
# 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 | ||
) |
There was a problem hiding this comment.
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)
There was a problem hiding this 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.
🥳 Successfully deployed to developer sandbox za. |
🥳 Successfully deployed to developer sandbox za. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
🥳 Successfully deployed to developer sandbox za. |
🥳 Successfully deployed to developer sandbox za. |
🥳 Successfully deployed to developer sandbox za. |
🥳 Successfully deployed to developer sandbox za. |
src/registrar/views/application.py
Outdated
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 |
There was a problem hiding this comment.
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
src/registrar/views/application.py
Outdated
# 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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this 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", |
There was a problem hiding this comment.
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."): |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
src/registrar/views/application.py
Outdated
# 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) |
There was a problem hiding this comment.
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
src/registrar/views/application.py
Outdated
return response | ||
|
||
def _get_orphaned_contacts(self, application: DomainApplication, check_db=True): | ||
"""Collects all orphaned contacts""" |
There was a problem hiding this comment.
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""" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
Co-authored-by: Alysia Broddrick <109625347+abroddrick@users.noreply.github.com>
Co-authored-by: Alysia Broddrick <109625347+abroddrick@users.noreply.github.com>
🥳 Successfully deployed to developer sandbox za. |
…com/cisagov/manage.get.gov into za/1501-users-delete-domain-records
🥳 Successfully deployed to developer sandbox za. |
🥳 Successfully deployed to developer sandbox za. |
Ticket
Resolves #1501
Changes
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 inWITHDRAWN
. 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
Ensured code standards are met (Original Developer)
Validated user-facing changes (if applicable)
As a code reviewer, I have
Reviewed, tested, and left feedback about the changes
Ensured code standards are met (Code reviewer)
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)
(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
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)
(Rarely needed) Tested as both an analyst and applicant user
Screenshots