#4422: dnsrecord edit form save [dg]#4746
Conversation
There was a problem hiding this comment.
When moving the swap to the outer div, it changed how Alpine tracked the dropdown state across htmx swaps. x-on:change handler was now on the form content which gets swapped by htmx - meaning after a swap the handler was on a fresh element that Alpine needed to reinitialize. The x-init was added to ensure Alpine resynced recordType after that reinitialization.
There was a problem hiding this comment.
Added the edit path:
- edit submissions return just the updated display row rather than injecting a new row into the table
- Replaced
counter ==1 first-record with an explicitis_first_recordboolean from the view
There was a problem hiding this comment.
this was reset back to counter
There was a problem hiding this comment.
ANDI FIX: Edit branch now OOB-swaps the data (via update_cells) instead of the whole row, the edit form row is always OOB-swapped (for errors or reset)
kimallen
left a comment
There was a problem hiding this comment.
I worked through the server side code. I still need to look at the template/htmx work and test in the app, but I wanted to publish early comments.
I'm noticing a couple places where there's aberration from existing patterns (naming, where we put db query methods), so would be interested in knowing if there's strong reasons for changing those patterns.
| logger.error(f"Error {e.response.status_code} while creating dns record: {e}") | ||
| raise | ||
| error_body = e.response.text | ||
| logger.error(f"Error {e.response.status_code} while creating dns record: {e}\nResponse body: {error_body}") |
There was a problem hiding this comment.
Nice- thanks for expanding what we log. This will help with error handling.
| raise | ||
| logger.info("Successfully enrolled %s in DNS hosting", domain_name) | ||
|
|
||
| def get_dns_record(self, domain, record_pk: int) -> DnsRecord | None: |
There was a problem hiding this comment.
Since this is purely a lookup, I wonder if this would better fit to fall under the DnsRecord Model? Is there a reason to put it here instead of the model? (same question for all of these methods)
There was a problem hiding this comment.
Ah yes that would fit better. I'll move them over.
| """Find an item by name in a list of dictionaries.""" | ||
| return next((item.get("id") for item in items if item.get("name") == name), None) | ||
|
|
||
| def _parse_dns_record_id(self, request) -> int | None: |
There was a problem hiding this comment.
Can you say more about why this is necessary, and why we can't just get the id directly from the request.POST?
There was a problem hiding this comment.
The id is only present in the POST for edit submissions
| <tr x-show="showFormId == {{ counter }}" class="hide-td-borders"> | ||
|
|
||
| {# Inline edit form row for a DNS record. `counter` is pk-based. #} | ||
| <tr id="dnsrecord-edit-row-{{ counter }}" x-show="showFormId == {{ counter }}" class="hide-td-borders" {% if oob_swap %}hx-swap-oob="outerHTML"{% endif %}> |
There was a problem hiding this comment.
syntax/naming: The hide-td-borders class is from legacy code, but I just noticed we call this hide-td-borders but aren't we technically hiding the row's overall border? Wondering how we ended up with that since we're using the class on the tr itself too 🤔
There was a problem hiding this comment.
I am not sure how to answer this. This was already part of the form just kept it.
There was a problem hiding this comment.
LMK if this is something that we now want to remove.
There was a problem hiding this comment.
@erinysong are you suggesting this should be named hide-tr-borders instead of hide-td-borders?
There was a problem hiding this comment.
I was asking mostly to ask if anyone else found the class name misleading/confusing since my first thought would've been that we're using this class to hide the border for the entire and not all the individual cells
There was a problem hiding this comment.
I am still unclear on the resolution for this. If I missed it please re-iterate.
As this was pre-existing, if we want to change it etc. we might best address that during a clean up of the styling or a final pass of the work (another ticket) vs this PR, which is scoped for the saving of the existing edit form.
There was a problem hiding this comment.
Great conversation and question to bring up!
| <tr x-show="showFormId == {{ counter }}" class="hide-td-borders"> | ||
|
|
||
| {# Inline edit form row for a DNS record. `counter` is pk-based. #} | ||
| <tr id="dnsrecord-edit-row-{{ counter }}" x-show="showFormId == {{ counter }}" class="hide-td-borders" {% if oob_swap %}hx-swap-oob="outerHTML"{% endif %}> |
There was a problem hiding this comment.
If we're identifying the rows by the record pk, why don't we just use the record.pk directly? Since counter implies that we're incrementally counting the rows themselves
There was a problem hiding this comment.
on that note, we used to use counter to identify our rows because initially we decided to label our rows by the order in which they appeared on the table to give allow the user to easily identify which row on the table they were on. I noticed in this PR we're removing that counter and the counter value is now the record pk. Can I hear more about why we're switching that value here?
There was a problem hiding this comment.
I didn't realize the counter had any use other than identifying really if it was the first row. Having the idea of the row be the ID is a pretty standard pattern for tables like this, especially since we're not allowing users to move their rows or anything like that so we don't need to keep track of order. Is there a requirement that we need to keep the order? If so we if so, we should probably add that to the model. I don't think I've seen it in there yet. I'll have to look again...
There was a problem hiding this comment.
hm yeah I've seen both incremental counters and the id of the data object be used. I think the incremental counters was an existing pattern from product decision to go incremental on form int identifiers for multiple portfolios because we didn't want to expose organization object id's. I'm not sure if we have that same level of concern for records so I'd defer to product if that's the case
There was a problem hiding this comment.
OH I didn't realize it was a over-arching decision on the project. Yeah I can change it back to do a counter thing.
There was a problem hiding this comment.
I think back then that decision was made specifically because we wanted to avoid exposing organization id's in our DOM, but I'm not sure if records would have that same scrutiny. Let's check in with product on Slack to see if that's the case! I can start a thread
There was a problem hiding this comment.
Per latest, updating to remove the counter and go back to the id.
| <tr x-show="showFormId == {{ counter }}" class="hide-td-borders"> | ||
|
|
||
| {# Inline edit form row for a DNS record. `counter` is pk-based. #} | ||
| <tr id="dnsrecord-edit-row-{{ counter }}" x-show="showFormId == {{ counter }}" class="hide-td-borders" {% if oob_swap %}hx-swap-oob="outerHTML"{% endif %}> |
There was a problem hiding this comment.
Also overall table design question from looking through dev tools: Can I hear more about why we load a hidden for each DNS row's edit form for each DNS record row specifically? My intuition would've been we only append/remove the row when we click the edit/cancel button for that one record row. So I'd be interested in hearing why we're loading hidden edit forms for each row 😮
There was a problem hiding this comment.
Since we only will ever have a couple of rows doing it this way the x-show avoids that extra round trip (network calls etc.) and the form with all its pre-filled field values is already in the DOM ready to display when the user clicks Edit.
There was a problem hiding this comment.
If we had hundreds of records or large records that we wouldn't do this and I'd use something like the htmx-fetched form on demand. But we're such a small record set, it is definitely a value add, and so we can take advantage of that and have the ability to lessen the load on the application and network.
There was a problem hiding this comment.
Got it and makes sense given we likely won't have too many records here. Thank you for the rundown!
There was a problem hiding this comment.
Observations when testing live (these are more around the form behavior and validations, so could potentially go in a different ticket):
- The validation alerts only show up at the top of the page, not within the edit form. Is that the desired behavior?
- Once opening the Edit form, I expected tabbing to enter the form. However, the next tab order was the "other options" and then it goes to the form fields. It seemed a bit confusing (especially with SR), and wondering if we want focus to go from the Edit button to the form directly (question for design?).
- When I edit record and make a required field empty, no alert shows up at the top of the page, and an html alert shows up instead.
We also seemed to have lost the fix that labeled the ids for each field uniquely per edit form. We now have the a11y error again where the labels can't find which field they are for. (so screen reader is not reading the label when focusing on an input)
| to display corresponding values in the table rows""" | ||
| for record in dns_records: | ||
| data_dict = self.record_dict_for_initial_data(record) | ||
| record.form = DomainDNSRecordForm(initial=data_dict, prefix=f"edit_{record.id}") |
There was a problem hiding this comment.
I added this line to correct issues in WAVE/ANDI with duplicate ids. Was this accidentally changed?
There was a problem hiding this comment.
Not accidentally. But I'll see about adding it back in / or something like it.
There was a problem hiding this comment.
Update to not use the interal ids and use the counter
I'll be OOO the next couple days and don't want to block this if changes get made. Abe is also on point to review, so he can check on follow up of my comments. It will still need a design review, so that can serve as the second reviewer.
There was a problem hiding this comment.
Changed ttl field from ChoiceField to TypedChoiceField(coerce=int) so the cleaned value is an integer, preventing a TypeError when the model's clean() method compares self.ttl < 60.
d91fc30 to
2dd63d1
Compare
|
|
||
| if errors: | ||
| raise ValidationError(errors) | ||
|
|
There was a problem hiding this comment.
Refactored view post calls into here and dhshostservice where appropriate.
There was a problem hiding this comment.
Refactored view post calls into here and dhshostservice where appropriate.
|
Hello team, I believe I've address all your comments except one that is open/unresolved by @Erin Song regarding the class="hide-td-borders". Reset all back to counters and I reused that to uniquely id the inputs, thus leaving internal ids un-exposed per the other Slack thread and comments. I went ahead and fixed the pickling errors as well on this PR bc it kept failing and there were real fixes needed especially in our area so those are in here too now. FYSA: I will OOO/traveling this week Mon AM -Fri AM. But I'll be on AM of Tuesday and Thursday. Thank you again for the revs. Cc: @Kim Allen [CTR] @Abraham Alam [CTR] @Erin Song (oh also I did send this over to design as well for this next rev round fysa) |
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| context_dns_record = ContextVar("context_dns_record", default=None) |
|
|
||
| return is_first_record, record_id | ||
|
|
||
| def post(self, request, *args, **kwargs): |
There was a problem hiding this comment.
Separated out the create, edit and form check into functions called from the post to fix flake 8 as the post was too complex.
| "update_cells": is_edit and self.dns_record is not None, | ||
| }, | ||
| headers={"HX-TRIGGER": hx_trigger_events}, | ||
| headers={"HX-Trigger-After-Settle": json.dumps({"messagesRefresh": "", "recordSubmitSuccess": ""})}, |
There was a problem hiding this comment.
for htmx swaps and transitions to be complete, before messagesRefresh and recordSubmitSuccess fire.
c367e61 to
f8cabaa
Compare
There was a problem hiding this comment.
- revert failure_cases to expect HTTPStatusError for methods that re-raise directly
- add dns_record_failure_cases with APIError for create/update dns record methods that wrap the error
- set mock_response.text to a real string so APIError message assertions pass
There was a problem hiding this comment.
expect APIError (not HTTPStatusError) for DNS record error cases since CloudflareService wraps HTTP errors in APIError for those methods
There was a problem hiding this comment.
- updated mock create_dns_record (the method the view actuallycalls)
- fix get_x_zone_id_if_zone_exists to return a list for nameservers instead of True, when nameservers = True (boolean), Django's template [domain_dns_nameservers_table.html] tries to iterates over True, so this would raise a TypeError during template rendering.
Co-authored-by: Kim Allen <kim@truss.works>
…pup, removed contextvar and refactored post() for flake8 fix, removed silencer Apply suggestion from @kimallen Co-authored-by: Kim Allen <kim@truss.works> cleanup more cleanup
…d POST field names
8320d9c to
bd742fa
Compare
| - Otherwise attach a blank/initial form from the DB model. | ||
| - Uses the record id to generate unique field IDs across multiple forms on the page. | ||
| """ | ||
| auto_id = f"id_edit_{record_obj.pk}_%s" |
There was a problem hiding this comment.
What does %s do in this string?
There was a problem hiding this comment.
string substitution - replaces the %s with the name of the form field
kimallen
left a comment
There was a problem hiding this comment.
Thanks so much for all your work and patience on this @DaisyGuti !
LGTM!
erinysong
left a comment
There was a problem hiding this comment.
Great work Daisy :D Thank you for working through all the changes and discussions we had around this PR!
Ticket
Resolves #4422
Changes
Setup
Please Note: DNS waffle flag must be on AND domain being work with must be enrolled in DNS hosting
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
Validated user-facing changes as a developer
Note: Multiple code reviewers can share the checklists above, a second reviewer should not make a duplicate checklist. All checks should be checked before approving, even those labeled N/A.
As a designer reviewer, I have
Verified that the changes match the design intention
Validated user-facing changes as a designer
References
Screenshots