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
Use form validation errors for important UI feedback #11095
Conversation
This includes a few fixes and small restructuring to the notification/message classes. - Centralize message lookup and format string population on the registry model instead of the Notification model. This change will make more sense with my next PR - Use `copy()` when setting the format values, as to ensure thread safety, these shouldn't be set on the static instance class in the registry. - Add a base class for notifications, for messages/notifications that fall outside doc building.
I found myself rebuilding these messages in template code and figured it would be a good excuse to further consolidate the patterns we're using. In short, these messages are emitted at view time and are not saved to the database. The view time logic is similar as notifications however. A few questions stand for me: - I like centering all of our notifications/errors around exceptions, as it retains an opportunity to emit exceptions from wherever we might want to move some of this logic. Arguably, this logic should not exist on a single view and could be moved on to modeling/queryset classes. - I don't have a strong opinion on whether it's important for these messages to exist in the registry. I see arguments for both, but I chose to use the registry so that we can consolidate logic there -- format values and message lookup, etc. I'm looking for input on these patterns. On the template side, the display is now just a simple `if` and template logic similar to the current notification display, using the header/body render methods.
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.
Thanks for opening this PR. I think the UX will be improved with these notifications.
I have conflicting feelings with the proposed pattern here. Mainly, because it's used in a way that it wasn't thought for: not saving the notification in the database. For those notifications, I think that Django messages or form validation errors are a better fit.
The pattern for the notification is:
- from builders, raise an exception from anywhere in the build process. It will be catch at
on_failure
and the API will be called to be saved in the db - from anywhere else with access to the db, just call
Notifications.objects.add()
This PR is adding another bullet point to the original pattern and removing one of the bases: saving the notification in the db. So, it seems that you are re-using the rendering but dropping the storage, making this "a one-time notification, each time the user opens that view".
I think what you want here is a "state-based notification", which we already have a pattern for that:
- check if some conditions are met and create a notification
- show the notification
- check if those conditions have changed (via Django signals), and
.cancel()
the notification
(see
readthedocs.org/readthedocs/projects/views/private.py
Lines 115 to 133 in 83805a6
def validate_primary_email(self, user): | |
""" | |
Sends a dismissable site notification to this user. | |
Checks if the user has a primary email or if the primary email | |
is verified or not. Sends a dismissable notification if | |
either of the condition is False. | |
""" | |
email_qs = user.emailaddress_set.filter(primary=True) | |
email = email_qs.first() | |
if not email or not email.verified: | |
Notification.objects.add( | |
attached_to=user, | |
message_id=MESSAGE_EMAIL_VALIDATION_PENDING, | |
dismissable=True, | |
format_values={ | |
"account_email_url": reverse("account_email"), | |
}, | |
) |
readthedocs.org/readthedocs/notifications/signals.py
Lines 51 to 68 in 83805a6
@receiver(post_save, sender=EmailAddress) | |
def user_email_verified(instance, *args, **kwargs): | |
"""Check if the primary email is validated and cancel the notification.""" | |
if instance.primary: | |
if instance.verified: | |
Notification.objects.cancel( | |
attached_to=instance.user, | |
message_id=MESSAGE_EMAIL_VALIDATION_PENDING, | |
) | |
else: | |
Notification.objects.add( | |
attached_to=instance.user, | |
message_id=MESSAGE_EMAIL_VALIDATION_PENDING, | |
dismissable=True, | |
format_values={ | |
"account_email_url": reverse("account_email"), | |
}, | |
) |
The only difference that I found here is that you want the notification to be shown only on a particular Django view instead of being attached to a particular object, which makes me think we should add another field Notification.view_name (default=None)
or similar and show them only on those specific views 👍🏼
I feel the pattern I'm describing here solves your problem, it's very clear and fits great in what we already have.
NOTE: I left some comments in the PR, but they are old now after writing this general review. I lean more towards the pattern I just describe here which is more explicit and clear.
# The following messages are added to the registry but are only used | ||
# directly by the project creation view template. These could be split out | ||
# directly for use by import, instead of reusing message id lookup. | ||
Message( |
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.
You can differentiate this also by splitting it into a new variable making it explicit that they are different from the messages
ones:
view_messages = [
# ...
]
registry.add(view_messages)
I used this pattern with MkDocs and other build notifications in other files, as well.
Also, I just realize that using this pattern will give us the opportunity of using API-based notification for this as well when we need them. |
Linking this PR readthedocs/ext-theme#274 since it's related to this backend changes. |
Yeah, a bit. Though Django messages don't work as they are now temporary warnings via FUI
Yes, this would work, but you are correct that we'd need more on the modeling. I didn't want to go this path as I'm not convinced this would be reusable or that it warrants complicating all of the rest of notifications. For this view, there need to be two individually addressable blocks too, one for automatic, and one manual. |
Thinking about all this more, I would say that complicating our modeling/notifications for just this view doesn't feel worth it. I would say what you're describing is closer to technically correct, but it does involve a lot of complication for such a minor use case. We are creating a new pattern in our notification system mostly just for this view in either plan, so I wouldn't avoid what I have here just on the basis of it being a new pattern. So, without taking on a lot of work here, the paths that I have here are:
The reason I came to use Messages here was that I felt it was better to re-use code, even if in a slightly new way, than it was to replicate work/patterns we've just consolidated. That is, I think it's a big win to move all our permanent/stateful messages to singular display logic, and I'm happy to extend our patterns to fit that. If you don't agree that re-using this pattern is okay, I'm just going to go back to approximating notifications one-off in template/view logic. I agree on the overall stateful notification pattern, but I don't think stateful per-view/per-block notifications on this view are that high on our needs/priorities.
What other views are you thinking of here? |
Thinking about re-using code is always good. I agree with that.
I don't want to go back to the template/view logic. Mainly, because removing the logic from there was one of the goals of the new notification system 😉
Honestly, I don't think that adding a new field to the
I haven't thought too much about this yet, but the UX you are describing here (blocking a UI because "something needs to be done first") seems pretty obvious to me. Thinking quickly about this, I image we can use this UX on Read the Docs for Business more for features that are based on plans. For example:
I think we will want to re-use this pattern sooner that what we are thinking now 😄 . This pattern will move away all this logic from the templates and backed it up in the backend re-using the normal notifications pattern we have 💯 . The more I write about this the more convinced I am 😅 Footnotes
|
I think there are some good thoughts there! I would need to think through this pattern a bit more first before committing to it though. This would be great to brainstorm more on I feel. I would probably agree the technical change wouldn't be huge initially, but I do see where this pattern would be high friction in development, at least compared to a simple template conditionals. I do want to get away from maintaining error messages and conditional logic in template code though. Some notifications make a lot of sense as stateful notifications -- notifications like subscription errors for example. This pattern feels low friction to me because we are going to show the subscription error notification on many different views and these notifications won't be altering these views besides display of the notification message. The use cases we're talking about here would be altering parts of individual views, and these notifications would be emit from signals that are unrelated to the view -- for example, on organization team change signal we emit/retract the notification for required admin permissions on project creation. That is, the answer to "How do I add/change a conditional to disable the project creation form?" seems like it would be:
Compared to Anyways, that's to say that I think we should continue playing with this pattern for notifications on high level messages on projects/organizations. For very acute template logic like what I am doing on the project creation page, the pattern isn't as clear of a win yet. |
What I found interesting in your list is that it seems like all of these examples, and my example here, probably do indeed fit best as form validation errors. So, maybe your original suggestion of using form validation errors is one to digest a bit more first. The examples you list a pretty straight forward. These views all use form instances in a very basic way, so it would only take emitting the validation error from a Form method. The project creation UI might need some convincing here, as the Form instance is hidden rather deeply in the HTML structure, controlled mostly through FUI elements. I will try this quickly to see. The big unknown here is if Form instances will display validation errors in the same way while the Form is still unbound. A minor point here is that validation errors only have a body, there is no header. I am liking how the headers on notifications can be concise and clear, with body to back up the header. Not a huge point though. What I think I will really appreciate with this pattern is that all of the control of the form function is now on the Form instance, and not split between form, view, and template logic. Edit: And as I expected, Form instances don't like having validation errors thrown while the form is still unbound. But this was pretty easy:
|
The need of changing the conditional is a good point that I hadn't considered.
If it's possible, I'd follow this path, yeah, that looks more Django-starndard.
I agree here. I don't think we require a header on these messages.
It seems you were able to figure this out in a simple way. I looks great to me 💯 . Do they render as you expected in the frontend? |
Sounds good! I might put together a basic implementation that I can work off of today and tomorrow, but might hand off some of this work afterwards. What I wrote above wasn't super clear, so to clarify: I am specifically talking about implementing Form classes that throw unbound form validation errors (forms that show errors on GET, before the user submits the form).
They did render, I just did a quick test outputting the
The |
- Prevalidaition on forms allows throwing errors on the initial form display, before the user submits the form. - Rich validation errors gives non-field error message types (info, error, warning, etc) and header/title text
Instead of using notifications without a database, use form validation errors to raise error states on specific forms, or outright disable them.
…roject-import-fixes
I was quite easily able to add both prevalidation on form instances and titled validation error messages. I spent some time refining the pattern and matching it to the What I really like is that all of the reasons why a form can't be valid are all in one place now. The implementation over It looks quite close to how the initial pass did, nothing much changed: And the supporting template changes are minimal and really straight forward: @humitos How does this approach feel now? Does this seem more reusable? |
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 am going to add a test or two to start and will move this from a draft PR. It seems this is close, I'd like to get some feedback on the permission checks before merging. |
I added some basic tests that the validation is working as expected for some of the important cases. Changes are the last few commits, starting with f38e7a8 |
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'm happy where this pattern ended up. I think it's great and follow Django standards 👍🏼 . It's ready to merge IMO.
I pinged Santos about the test cases because I think we are mixing two patterns that shouldn't be mixed, tho.
url = reverse("socialaccount_connections") | ||
raise RichValidationError( | ||
_( | ||
f"You must first <a href='{url}'>add a connected service " |
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 think this is rendered as text, not HTML, or at least the "normal" errors are. If we are rendering errors as HTML, we should be careful, there are other places where we include user input in them.
I found myself rebuilding these messages in template code and figured it would be a good excuse to further consolidate the patterns we're using.
In short, these messages are emitted at view time and are not saved to the database. The view time logic is similar as notifications however.
A few questions stand for me:
I'm looking for input on these patterns.
On the template side, the display is now just a simple
if
and template logic similar to the current notification display, using the header/body render methods.I don't know why GitHub is having such a hard time with the PR base, but the relevant commit here is: 1cf32db
Update: I'll leave the above, but the pattern here changed to using Form and validation errors