From 65f8bdc6c0a034f9416719a5e3ecf4d3f5f2ac4e Mon Sep 17 00:00:00 2001 From: Francesco Siddi Date: Sat, 5 Mar 2016 23:22:57 +0100 Subject: [PATCH] Introducing notifications --- pillar/application/__init__.py | 25 +++++ pillar/application/utils/activities.py | 132 +++++++++++++++++++++++++ pillar/settings.py | 41 ++++---- 3 files changed, 177 insertions(+), 21 deletions(-) create mode 100644 pillar/application/utils/activities.py diff --git a/pillar/application/__init__.py b/pillar/application/__init__.py index 8f5813de..178bba65 100644 --- a/pillar/application/__init__.py +++ b/pillar/application/__init__.py @@ -15,6 +15,7 @@ from eve import Eve from eve.auth import TokenAuth from eve.io.mongo import Validator + RFC1123_DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' @@ -118,6 +119,9 @@ from application.utils.authorization import check_permissions from application.utils.gcs import update_file_name from application.utils.algolia import algolia_index_user_save from application.utils.algolia import algolia_index_node_save +from application.utils.activities import activity_create +from application.utils.activities import activity_subscribe +# from application.utils.activities import notification_parse from modules.file_storage import process_file from modules.file_storage import delete_file from modules.file_storage import generate_link @@ -164,6 +168,22 @@ def before_inserting_nodes(items): if project: item['project'] = project['_id'] +def after_inserting_nodes(items): + for item in items: + activity_create(item['user'], 'node', item['_id']) + if item['node_type'] == 'comment': + verb = 'commented' + else: + verb = 'posted' + activity_subscribe( + item['user'], + verb, + 'node', + item['_id'], + 'node', + item['parent'] + ) + def item_parse_attachments(response): """Before returning a response, check if the 'attachments' property is defined. If yes, load the file (for the moment only images) in the required @@ -229,16 +249,21 @@ def project_node_type_has_method(response): if not check_permissions(node_type, 'GET', append_allowed_methods=True): return abort(403) +# def before_returning_notifications(response): +# for item in response['_items']: +# notification_parse(item) app.on_fetched_item_nodes += before_returning_item_permissions app.on_fetched_item_nodes += item_parse_attachments app.on_fetched_resource_nodes += before_returning_resource_permissions app.on_fetched_resource_nodes += resource_parse_attachments app.on_fetched_item_node_types += before_returning_item_permissions +# app.on_fetched_resource_notifications += before_returning_notifications app.on_fetched_resource_node_types += before_returning_resource_permissions app.on_replace_nodes += before_replacing_node app.on_replaced_nodes += after_replacing_node app.on_insert_nodes += before_inserting_nodes +app.on_inserted_nodes += after_inserting_nodes app.on_fetched_item_projects += before_returning_item_permissions app.on_fetched_item_projects += project_node_type_has_method app.on_fetched_resource_projects += before_returning_resource_permissions diff --git a/pillar/application/utils/activities.py b/pillar/application/utils/activities.py new file mode 100644 index 00000000..86556ac7 --- /dev/null +++ b/pillar/application/utils/activities.py @@ -0,0 +1,132 @@ +from eve.methods.post import post_internal +from application import app + +# def notification_parse(notification): +# # TODO: finish fixing this +# activities_collection = app.data.driver.db['activities'] +# users_collection = app.data.driver.db['users'] +# nodes_collection = app.data.driver.db['nodes'] +# activity = activities_collection.find_one({'_id': notification['_id']}) +# actor = users_collection.find_one({'_id': activity['actor_user']}) +# # Context is optional +# context_object_type = None +# context_object_name = None +# context_object_url = None + +# if activity['object_type'] == 'node': +# node = nodes_collection.find_one({'_id': activity['object']}) +# # project = Project.find(node.project, { +# # 'projection': '{"name":1, "url":1}'}, api=api) +# # Initial support only for node_type comments +# if node['node_type'] == 'comment': +# # comment = Comment.query.get_or_404(notification_object.object_id) +# node['parent'] = nodes_collection.find_one({'_id': node['parent']}) +# object_type = 'comment' +# object_name = '' + +# object_url = url_for('nodes.view', node_id=node._id, redir=1) +# if node.parent.user == current_user.objectid: +# owner = "your {0}".format(node.parent.node_type) +# else: +# parent_comment_user = User.find(node.parent.user, api=api) +# owner = "{0}'s {1}".format(parent_comment_user.username, +# node.parent.node_type) + +# context_object_type = node.parent.node_type +# context_object_name = owner +# context_object_url = url_for('nodes.view', node_id=node.parent._id, redir=1) +# if activity.verb == 'replied': +# action = 'replied to' +# elif activity.verb == 'commented': +# action = 'left a comment on' +# else: +# action = activity.verb +# else: +# return None +# else: +# return None + +# return dict( +# _id=notification._id, +# username=actor.username, +# username_avatar=actor.gravatar(), +# action=action, +# object_type=object_type, +# object_name=object_name, +# object_url=object_url, +# context_object_type=context_object_type, +# context_object_name=context_object_name, +# context_object_url=context_object_url, +# date=pretty_date(activity._created), +# is_read=notification.is_read, +# # is_subscribed=notification.is_subscribed +# ) + +def notification_get_subscriptions(context_object_type, context_object_id, actor_user_id): + subscriptions_collection = app.data.driver.db['activities-subscriptions'] + lookup = { + 'user': {"$ne": actor_user_id}, + 'context_object_type': context_object_type, + 'context_object': context_object_id, + 'is_subscribed': True, + } + print lookup + return subscriptions_collection.find(lookup) + + +def activity_create(user_id, context_object_type, context_object_id): + """Subscribe a user to changes for a specific context. We create a subscription + if none is found. + + :param user_id: id of the user we are going to subscribe + :param context_object_type: hardcoded index, check the notifications/model.py + :param context_object_id: object id, to be traced with context_object_type_id + """ + subscriptions_collection = app.data.driver.db['activities-subscriptions'] + lookup = { + 'user': user_id, + 'context_object_type': context_object_type, + 'context_object': context_object_id + } + subscription = subscriptions_collection.find_one(lookup) + + # If no subscription exists, we create one + if not subscription: + post_internal('activities-subscriptions', lookup) + + +def activity_subscribe(actor_user_id, verb, object_type, object_id, + context_object_type, context_object_id): + """Add a notification object and creates a notification for each user that + - is not the original author of the post + - is actively subscribed to the object + + This works using the following pattern: + + ACTOR -> VERB -> OBJECT -> CONTEXT + + :param actor_user_id: id of the user who is changing the object + :param verb: the action on the object ('commented', 'replied') + :param object_type: hardcoded name + :param object_id: object id, to be traced with object_type_id + """ + + subscriptions = notification_get_subscriptions( + context_object_type, context_object_id, actor_user_id) + + if subscriptions.count(): + activity = dict( + actor_user=actor_user_id, + verb=verb, + object_type=object_type, + object=object_id, + context_object_type=context_object_type, + context_object=context_object_id + ) + + activity = post_internal('activities', activity) + for subscription in subscriptions: + notification = dict( + user=subscription['user'], + activity=activity[0]['_id']) + post_internal('notifications', notification) diff --git a/pillar/settings.py b/pillar/settings.py index 7c1d56b9..62f4c136 100644 --- a/pillar/settings.py +++ b/pillar/settings.py @@ -20,6 +20,16 @@ _file_embedded_schema = { } } +_required_user_embedded_schema = { + 'type': 'objectid', + 'required': True, + 'data_relation': { + 'resource': 'users', + 'field': '_id', + 'embeddable': True + }, +} + _activity_object_type = { 'type': 'string', 'required': True, @@ -251,7 +261,6 @@ nodes_schema = { }, 'picture': { 'type': 'objectid', - 'nullable': True, 'data_relation': { 'resource': 'files', 'field': '_id', @@ -629,10 +638,7 @@ projects_schema = { } activities_subscriptions_schema = { - 'user': { - 'type': 'objectid', - 'required': True - }, + 'user': _required_user_embedded_schema, 'context_object_type': _activity_object_type, 'context_object': { 'type': 'objectid', @@ -646,16 +652,18 @@ activities_subscriptions_schema = { }, 'web': { 'type': 'boolean', + 'default': True }, } + }, + 'is_subscribed': { + 'type': 'boolean', + 'default': True } } activities_schema = { - 'actor_user': { - 'type': 'objectid', - 'required': True - }, + 'actor_user': _required_user_embedded_schema, 'verb': { 'type': 'string', 'required': True @@ -673,13 +681,10 @@ activities_schema = { } notifications_schema = { - 'user': { - 'type': 'objectid', - 'required': True - }, + 'user': _required_user_embedded_schema, 'activity': { 'type': 'objectid', - 'required': True + 'required': True, }, 'is_read': { 'type': 'boolean', @@ -752,20 +757,14 @@ projects = { activities = { 'schema': activities_schema, - 'public_item_methods': None, - 'public_methods': None } activities_subscriptions = { 'schema': activities_subscriptions_schema, - 'public_item_methods': None, - 'public_methods': None } notifications = { - 'schema': activities_subscriptions_schema, - 'public_item_methods': None, - 'public_methods': None + 'schema': notifications_schema, }