Notifications backend - #1904
Conversation
Keep it for now with the minimal features required.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1904 +/- ##
==========================================
+ Coverage 79.94% 80.48% +0.53%
==========================================
Files 83 86 +3
Lines 7481 7782 +301
==========================================
+ Hits 5981 6263 +282
- Misses 1500 1519 +19
🚀 New features to boost your workflow:
|
gpetretto
left a comment
There was a problem hiding this comment.
Overall it looks good to me. I left some comments.
|
|
||
| if flask_mongo.db.users.find_one( | ||
| {"_id": recipient_object_id}, | ||
| {"_id": 1}, |
There was a problem hiding this comment.
For the option to send to all recipients the selected users are active. Should it be used also here in the search? Or is it fine even if it sent to inactive users?
There was a problem hiding this comment.
I guess here I would allow even deactivated users as it is an explicit send. In principle deactivated users are allowed to authenticate but not allowed to edit. I'd maybe refer to @ml-evs about what he thinks about it when we undraft the PR
There was a problem hiding this comment.
ok. In practice I don't think it makes really a big difference. The only difference would be for the sender of the notification that does not get a warning that the user is deactivated.
There was a problem hiding this comment.
Ok, I'll keep this conversation open for now
|
Thanks for the review @gpetretto I think I addressed the comments or asked some question. Feel free to resolve or propose anything you deem right. |
|
This is ready for review @ml-evs @be-smith @OMWalmsley I addressed @gpetretto 's comments, with one being left up for discussion. As mentioned in the main text of the PR, this is only the backend part for the notifications. I will make the frontend in another PR when this is approved and merged. Note that I have already made a working version of the frontend that uses all this backend functionality. I can show it in case this helps of course. |
OMWalmsley
left a comment
There was a problem hiding this comment.
I think it generally looks good, just a few improvements that could be done in the notifications route.
| created_by = current_user.person.immutable_id | ||
|
|
||
| def create_for_recipients(session=None): | ||
| notification_results = [] |
There was a problem hiding this comment.
notification_results shadows name from outer scope
|
|
||
| def create_for_recipients(session=None): | ||
| notification_results = [] | ||
| for recipient_id in recipient_ids: |
There was a problem hiding this comment.
recipient_id shadows name from outer scope
| grouping=grouping, | ||
| session=session, | ||
| ) | ||
| except (ValidationError, ValueError) as exc: |
There was a problem hiding this comment.
exc also shadows name from outer scope
| try: | ||
| hello = flask_mongo.cx.admin.command("hello") | ||
| supports_transactions = hello.get("msg") == "isdbgrid" or "setName" in hello | ||
| except Exception: |
There was a problem hiding this comment.
Is it possible to narrow down what this Exception should be?
- (e.g.
pymongo.errors.PyMongoError)
| except Exception: | ||
| supports_transactions = False | ||
|
|
||
| if supports_transactions: |
There was a problem hiding this comment.
supports_transactions could be removed completely and the body of this if statement could be moved into else statement after try...except
There was a problem hiding this comment.
Can be indeed. I did it as it is probably cleaner. I would just wondering initially that we could actually make this test once at deployment time somehow and not having to test it everytime (and it would/could be read from current deployment config file or similar). Not sure if that's worth anyway so I propose we keep it as you propose for now.
| with session.start_transaction(): | ||
| notification_results = create_for_recipients(session=session) | ||
| else: | ||
| notification_results = create_for_recipients() |
There was a problem hiding this comment.
Could move this into the except statement
There was a problem hiding this comment.
Refactored (see above comment)
| @NOTIFICATIONS.route("/notifications/<notification_id>", methods=["PATCH"]) | ||
| @notification_recipient_only | ||
| def update_notification( | ||
| notification_id: str, |
There was a problem hiding this comment.
Similar to other comment (about notification_id).
| @NOTIFICATIONS.route("/notifications/<notification_id>", methods=["DELETE"]) | ||
| @notification_recipient_only | ||
| def delete_notification( | ||
| notification_id: str, |
There was a problem hiding this comment.
notification_id is not used in this function, could possibly have decorator (@notification_recipient_only) remove this from **kwargs. Since the only two instances using it, do not make use of the notification_id.
Removed supports_transaction and refactored the try transaction and fallback to no transactions part.
update_notification. Consumes notification_id from the kwargs in the notification_recipient_only decorator. Narrowed down exceptions in try/except for ObjectId.
davidwaroquiers
left a comment
There was a problem hiding this comment.
@OMWalmsley Thanks for the comments. I think I addressed all of them.
| created_by = current_user.person.immutable_id | ||
|
|
||
| def create_for_recipients(session=None): | ||
| notification_results = [] |
|
|
||
| def create_for_recipients(session=None): | ||
| notification_results = [] | ||
| for recipient_id in recipient_ids: |
| grouping=grouping, | ||
| session=session, | ||
| ) | ||
| except (ValidationError, ValueError) as exc: |
| try: | ||
| hello = flask_mongo.cx.admin.command("hello") | ||
| supports_transactions = hello.get("msg") == "isdbgrid" or "setName" in hello | ||
| except Exception: |
| except Exception: | ||
| supports_transactions = False | ||
|
|
||
| if supports_transactions: |
There was a problem hiding this comment.
Can be indeed. I did it as it is probably cleaner. I would just wondering initially that we could actually make this test once at deployment time somehow and not having to test it everytime (and it would/could be read from current deployment config file or similar). Not sure if that's worth anyway so I propose we keep it as you propose for now.
| with session.start_transaction(): | ||
| notification_results = create_for_recipients(session=session) | ||
| else: | ||
| notification_results = create_for_recipients() |
There was a problem hiding this comment.
Refactored (see above comment)
| @NOTIFICATIONS.route("/notifications/<notification_id>", methods=["PATCH"]) | ||
| @notification_recipient_only | ||
| def update_notification( | ||
| notification_id: str, |
| @NOTIFICATIONS.route("/notifications/<notification_id>", methods=["DELETE"]) | ||
| @notification_recipient_only | ||
| def delete_notification( | ||
| notification_id: str, |
Thanks for your review. I believe I've addressed your comments. Feel free to check and resolve if it looks fine to you. |
Backend notifications API
What this PR does
This adds the backend pieces for in-app notifications (see #1875)
Notifications are stored per user/recipient: even when one API request targets multiple users, the backend creates one notification document per recipient. This is intentional, because it keeps read/archive/delete state user-specific and avoids shared notification state becoming awkward. We are also not foreseeing a lot of messages that are sent to a lot of users except for update/maintenance notifications.
The feature is behind
ENABLE_NOTIFICATIONS, which is still disabled by default. When enabled, the backend exposes routes to create, list, read/unread, archive/unarchive, delete, count unread notifications, and mark everything as read.The PR also adds grouped notifications, so repeated events from the same source can update one existing notification instead of creating a new notification every time.
How to use it
Currently, only admins can create notifications with:
To send to every active user:
Users can then use:
Users can only access their own notifications.
Grouping behavior
A notification can be grouped by passing a grouping object:
Grouping "identity" is based on:
Archived notifications are not reused.
For grouped notifications:
Notification levels
Current notification levels are:
They have explicit priorities in the model, so grouped notifications can keep the highest-priority level.
No frontend changes are included in this PR.
The helper API create_notification(...) remains available for direct backend callers.