Notifications regression: Notifications not created
Notifications for when someone posted a comment on your node was not created. Root cause was that default values defined in schema was not set, resulting in activity subscriptions not being active. There were 2 bugs preventing them to be set: * The way the caching of markdown as html was implemented caused default values not to be set. * Eve/Cerberus regression causes nested default values to fail https://github.com/pyeve/eve/issues/1174 Also, a 3rd bug caused nodes without a parent not to have a subscription. Migration scripts: How markdown fields is cached has changed, and unused properties of attachments has been removed. ./manage.py maintenance replace_pillar_node_type_schemas Set the default values of activities-subscription ./manage.py maintenance fix_missing_activities_subscription_defaults
This commit is contained in:
@@ -4,6 +4,8 @@ This'll help us upgrade to new versions of Cerberus.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from pillar.api.node_types.utils import markdown_fields
|
||||
from pillar.tests import AbstractPillarTest
|
||||
|
||||
from bson import ObjectId
|
||||
@@ -219,10 +221,9 @@ class MarkdownValidatorTest(AbstractSchemaValidationTest):
|
||||
'schema': {
|
||||
'type': 'dict',
|
||||
'schema': {
|
||||
'content': {'type': 'string', 'required': True, 'validator': 'markdown'},
|
||||
'_content_html': {'type': 'string'},
|
||||
'descr': {'type': 'string', 'required': True, 'validator': 'markdown'},
|
||||
'_descr_html': {'type': 'string'},
|
||||
**markdown_fields('content', required=True),
|
||||
**markdown_fields('descr', required=True),
|
||||
'my_default_value': {'type': 'string', 'default': 'my default value'},
|
||||
}
|
||||
},
|
||||
}}
|
||||
@@ -239,6 +240,7 @@ class MarkdownValidatorTest(AbstractSchemaValidationTest):
|
||||
'_content_html': '<h1>Header</h1>\n<p>Some text</p>\n',
|
||||
'descr': 'je moeder',
|
||||
'_descr_html': '<p>je moeder</p>\n',
|
||||
'my_default_value': 'my default value'
|
||||
}]}
|
||||
|
||||
self.assertEqual(expect, doc)
|
||||
self.assertEqual(expect, self.validator.document)
|
||||
|
193
tests/test_api/test_notifications.py
Normal file
193
tests/test_api/test_notifications.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import bson
|
||||
import flask
|
||||
from bson import ObjectId
|
||||
|
||||
from pillar.tests import AbstractPillarTest
|
||||
|
||||
|
||||
class NotificationsTest(AbstractPillarTest):
|
||||
def setUp(self, **kwargs):
|
||||
super().setUp(**kwargs)
|
||||
self.project_id, _ = self.ensure_project_exists()
|
||||
self.user1_id = self.create_user(user_id=str(bson.ObjectId()))
|
||||
self.user2_id = self.create_user(user_id=str(bson.ObjectId()))
|
||||
|
||||
def test_create_node(self):
|
||||
"""When a node is created, a subscription should also be created"""
|
||||
|
||||
with self.app.app_context():
|
||||
self.login_api_as(self.user1_id, roles={'subscriber', 'admin'},
|
||||
# This group is hardcoded in the EXAMPLE_PROJECT.
|
||||
group_ids=[ObjectId('5596e975ea893b269af85c0e')])
|
||||
|
||||
node_id = self.post_node(self.user1_id)
|
||||
|
||||
self.assertSubscribed(node_id, self.user1_id)
|
||||
self.assertNotSubscribed(node_id, self.user2_id)
|
||||
|
||||
def test_comment_on_own_node(self):
|
||||
"""A comment on my own node should not give me any notifications"""
|
||||
|
||||
with self.app.app_context():
|
||||
self.login_api_as(self.user1_id, roles={'subscriber', 'admin'},
|
||||
# This group is hardcoded in the EXAMPLE_PROJECT.
|
||||
group_ids=[ObjectId('5596e975ea893b269af85c0e')])
|
||||
|
||||
node_id = self.post_node(self.user1_id)
|
||||
comment_id = self.post_comment(node_id)
|
||||
|
||||
self.assertSubscribed(comment_id, self.user1_id)
|
||||
self.assertNotSubscribed(comment_id, self.user2_id)
|
||||
|
||||
with self.login_as(self.user1_id):
|
||||
notification = self.notification_for_object(comment_id)
|
||||
self.assertIsNone(notification)
|
||||
|
||||
def test_comment_on_node(self):
|
||||
"""A comment on some one else's node should give them a notification, and subscriptions should be created"""
|
||||
|
||||
with self.app.app_context():
|
||||
self.login_api_as(self.user1_id, roles={'subscriber', 'admin'},
|
||||
# This group is hardcoded in the EXAMPLE_PROJECT.
|
||||
group_ids=[ObjectId('5596e975ea893b269af85c0e')])
|
||||
|
||||
node_id = self.post_node(self.user1_id)
|
||||
|
||||
with self.app.app_context():
|
||||
self.login_api_as(self.user2_id, roles={'subscriber', 'admin'},
|
||||
# This group is hardcoded in the EXAMPLE_PROJECT.
|
||||
group_ids=[ObjectId('5596e975ea893b269af85c0e')])
|
||||
comment_id = self.post_comment(node_id)
|
||||
|
||||
self.assertSubscribed(comment_id, self.user2_id)
|
||||
self.assertNotSubscribed(comment_id, self.user1_id)
|
||||
self.assertSubscribed(node_id, self.user1_id, self.user2_id)
|
||||
|
||||
with self.login_as(self.user1_id):
|
||||
notification = self.notification_for_object(comment_id)
|
||||
self.assertIsNotNone(notification)
|
||||
|
||||
def test_mark_notification_as_read(self):
|
||||
with self.app.app_context():
|
||||
self.login_api_as(self.user1_id, roles={'subscriber', 'admin'},
|
||||
# This group is hardcoded in the EXAMPLE_PROJECT.
|
||||
group_ids=[ObjectId('5596e975ea893b269af85c0e')])
|
||||
|
||||
node_id = self.post_node(self.user1_id)
|
||||
|
||||
with self.app.app_context():
|
||||
self.login_api_as(self.user2_id, roles={'subscriber', 'admin'},
|
||||
# This group is hardcoded in the EXAMPLE_PROJECT.
|
||||
group_ids=[ObjectId('5596e975ea893b269af85c0e')])
|
||||
comment_id = self.post_comment(node_id)
|
||||
|
||||
with self.login_as(self.user1_id):
|
||||
notification = self.notification_for_object(comment_id)
|
||||
self.assertFalse(notification['is_read'])
|
||||
|
||||
is_read_toggle_url = flask.url_for('notifications.action_read_toggle', notification_id=notification['_id'])
|
||||
self.get(is_read_toggle_url)
|
||||
|
||||
notification = self.notification_for_object(comment_id)
|
||||
self.assertTrue(notification['is_read'])
|
||||
|
||||
self.get(is_read_toggle_url)
|
||||
|
||||
notification = self.notification_for_object(comment_id)
|
||||
self.assertFalse(notification['is_read'])
|
||||
|
||||
def test_unsubscribe(self):
|
||||
"""It should be possible to unsubscribe to notifications"""
|
||||
with self.app.app_context():
|
||||
self.login_api_as(self.user1_id, roles={'subscriber', 'admin'},
|
||||
# This group is hardcoded in the EXAMPLE_PROJECT.
|
||||
group_ids=[ObjectId('5596e975ea893b269af85c0e')])
|
||||
|
||||
node_id = self.post_node(self.user1_id)
|
||||
|
||||
with self.app.app_context():
|
||||
self.login_api_as(self.user2_id, roles={'subscriber', 'admin'},
|
||||
# This group is hardcoded in the EXAMPLE_PROJECT.
|
||||
group_ids=[ObjectId('5596e975ea893b269af85c0e')])
|
||||
comment_id = self.post_comment(node_id)
|
||||
|
||||
with self.login_as(self.user1_id):
|
||||
notification = self.notification_for_object(comment_id)
|
||||
self.assertTrue(notification['is_subscribed'])
|
||||
|
||||
is_subscribed_toggle_url =\
|
||||
flask.url_for('notifications.action_subscription_toggle', notification_id=notification['_id'])
|
||||
self.get(is_subscribed_toggle_url)
|
||||
|
||||
notification = self.notification_for_object(comment_id)
|
||||
self.assertFalse(notification['is_subscribed'])
|
||||
|
||||
with self.app.app_context():
|
||||
self.login_api_as(self.user2_id, roles={'subscriber', 'admin'},
|
||||
# This group is hardcoded in the EXAMPLE_PROJECT.
|
||||
group_ids=[ObjectId('5596e975ea893b269af85c0e')])
|
||||
comment2_id = self.post_comment(node_id)
|
||||
|
||||
with self.login_as(self.user1_id):
|
||||
notification = self.notification_for_object(comment2_id)
|
||||
self.assertFalse(notification['is_subscribed'])
|
||||
|
||||
def assertSubscribed(self, node_id, *user_ids):
|
||||
subscriptions_col = self.app.data.driver.db['activities-subscriptions']
|
||||
|
||||
lookup = {
|
||||
'context_object': node_id,
|
||||
'notifications.web': True,
|
||||
}
|
||||
subscriptions = list(subscriptions_col.find(lookup))
|
||||
self.assertEquals(len(subscriptions), len(user_ids))
|
||||
for s in subscriptions:
|
||||
self.assertIn(s['user'], user_ids)
|
||||
self.assertEquals(s['context_object_type'], 'node')
|
||||
|
||||
def assertNotSubscribed(self, node_id, user_id):
|
||||
subscriptions_col = self.app.data.driver.db['activities-subscriptions']
|
||||
|
||||
lookup = {
|
||||
'context_object': node_id,
|
||||
}
|
||||
subscriptions = subscriptions_col.find(lookup)
|
||||
for s in subscriptions:
|
||||
self.assertNotEquals(s['user'], user_id)
|
||||
|
||||
def notification_for_object(self, node_id):
|
||||
notifications_url = flask.url_for('notifications.index')
|
||||
notification_items = self.get(notifications_url).json['items']
|
||||
|
||||
object_url = flask.url_for('nodes.redirect_to_context', node_id=str(node_id), _external=False)
|
||||
for candidate in notification_items:
|
||||
if candidate['object_url'] == object_url:
|
||||
return candidate
|
||||
|
||||
def post_node(self, user_id):
|
||||
node_doc = {'description': '',
|
||||
'project': self.project_id,
|
||||
'node_type': 'asset',
|
||||
'user': user_id,
|
||||
'properties': {'status': 'published',
|
||||
'tags': [],
|
||||
'order': 0,
|
||||
'categories': '',
|
||||
},
|
||||
'name': 'My first test node'}
|
||||
|
||||
r, _, _, status = self.app.post_internal('nodes', node_doc)
|
||||
self.assertEqual(status, 201, r)
|
||||
node_id = r['_id']
|
||||
return node_id
|
||||
|
||||
def post_comment(self, node_id):
|
||||
comment_url = flask.url_for('nodes_api.post_node_comment', node_path=str(node_id))
|
||||
comment = self.post(
|
||||
comment_url,
|
||||
json={
|
||||
'msg': 'je möder lives at [home](https://cloud.blender.org/)',
|
||||
},
|
||||
expected_status=201,
|
||||
)
|
||||
return ObjectId(comment.json['id'])
|
@@ -114,147 +114,6 @@ class UpgradeAttachmentSchemaTest(AbstractPillarTest):
|
||||
self.assertEqual({}, db_node_type['form_schema'])
|
||||
|
||||
|
||||
class UpgradeAttachmentUsageTest(AbstractPillarTest):
|
||||
def setUp(self, **kwargs):
|
||||
super().setUp(**kwargs)
|
||||
self.pid, self.uid = self.create_project_with_admin(user_id=24 * 'a')
|
||||
|
||||
with self.app.app_context():
|
||||
files_coll = self.app.db('files')
|
||||
|
||||
res = files_coll.insert_one({
|
||||
**ctd.EXAMPLE_FILE,
|
||||
'project': self.pid,
|
||||
'user': self.uid,
|
||||
})
|
||||
self.fid = res.inserted_id
|
||||
|
||||
def test_image_link(self):
|
||||
with self.app.app_context():
|
||||
nodes_coll = self.app.db('nodes')
|
||||
res = nodes_coll.insert_one({
|
||||
**ctd.EXAMPLE_NODE,
|
||||
'picture': self.fid,
|
||||
'project': self.pid,
|
||||
'user': self.uid,
|
||||
'description': "# Title\n\n@[slug 0]\n@[slug1]\n@[slug2]\nEitje van Fabergé.",
|
||||
'properties': {
|
||||
'status': 'published',
|
||||
'content_type': 'image',
|
||||
'file': self.fid,
|
||||
'attachments': {
|
||||
'slug 0': {
|
||||
'oid': self.fid,
|
||||
'link': 'self',
|
||||
},
|
||||
'slug1': {
|
||||
'oid': self.fid,
|
||||
'link': 'custom',
|
||||
'link_custom': 'https://cloud.blender.org/',
|
||||
},
|
||||
'slug2': {
|
||||
'oid': self.fid,
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
nid = res.inserted_id
|
||||
|
||||
from pillar.cli.maintenance import upgrade_attachment_usage
|
||||
|
||||
with self.app.app_context():
|
||||
upgrade_attachment_usage(proj_url=ctd.EXAMPLE_PROJECT['url'], go=True)
|
||||
node = nodes_coll.find_one({'_id': nid})
|
||||
|
||||
self.assertEqual(
|
||||
"# Title\n\n"
|
||||
"{attachment 'slug-0' link='self'}\n"
|
||||
"{attachment 'slug1' link='https://cloud.blender.org/'}\n"
|
||||
"{attachment 'slug2'}\n"
|
||||
"Eitje van Fabergé.",
|
||||
node['description'],
|
||||
'The description should be updated')
|
||||
self.assertEqual(
|
||||
"<h1>Title</h1>\n"
|
||||
"<!-- {attachment 'slug-0' link='self'} -->\n"
|
||||
"<!-- {attachment 'slug1' link='https://cloud.blender.org/'} -->\n"
|
||||
"<!-- {attachment 'slug2'} -->\n"
|
||||
"<p>Eitje van Fabergé.</p>\n",
|
||||
node['_description_html'],
|
||||
'The _description_html should be updated')
|
||||
|
||||
self.assertEqual(
|
||||
{'slug-0': {'oid': self.fid},
|
||||
'slug1': {'oid': self.fid},
|
||||
'slug2': {'oid': self.fid},
|
||||
},
|
||||
node['properties']['attachments'],
|
||||
'The link should have been removed from the attachment')
|
||||
|
||||
def test_post(self):
|
||||
"""This requires checking the dynamic schema of the node."""
|
||||
with self.app.app_context():
|
||||
nodes_coll = self.app.db('nodes')
|
||||
res = nodes_coll.insert_one({
|
||||
**ctd.EXAMPLE_NODE,
|
||||
'node_type': 'post',
|
||||
'project': self.pid,
|
||||
'user': self.uid,
|
||||
'picture': self.fid,
|
||||
'description': "meh",
|
||||
'properties': {
|
||||
'status': 'published',
|
||||
'content': "# Title\n\n@[slug0]\n@[slug1]\n@[slug2]\nEitje van Fabergé.",
|
||||
'attachments': {
|
||||
'slug0': {
|
||||
'oid': self.fid,
|
||||
'link': 'self',
|
||||
},
|
||||
'slug1': {
|
||||
'oid': self.fid,
|
||||
'link': 'custom',
|
||||
'link_custom': 'https://cloud.blender.org/',
|
||||
},
|
||||
'slug2': {
|
||||
'oid': self.fid,
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
nid = res.inserted_id
|
||||
|
||||
from pillar.cli.maintenance import upgrade_attachment_usage
|
||||
|
||||
with self.app.app_context():
|
||||
upgrade_attachment_usage(proj_url=ctd.EXAMPLE_PROJECT['url'], go=True)
|
||||
node = nodes_coll.find_one({'_id': nid})
|
||||
|
||||
self.assertEqual(
|
||||
"# Title\n\n"
|
||||
"{attachment 'slug0' link='self'}\n"
|
||||
"{attachment 'slug1' link='https://cloud.blender.org/'}\n"
|
||||
"{attachment 'slug2'}\n"
|
||||
"Eitje van Fabergé.",
|
||||
node['properties']['content'],
|
||||
'The content should be updated')
|
||||
self.assertEqual(
|
||||
"<h1>Title</h1>\n"
|
||||
"<!-- {attachment 'slug0' link='self'} -->\n"
|
||||
"<!-- {attachment 'slug1' link='https://cloud.blender.org/'} -->\n"
|
||||
"<!-- {attachment 'slug2'} -->\n"
|
||||
"<p>Eitje van Fabergé.</p>\n",
|
||||
node['properties']['_content_html'],
|
||||
'The _content_html should be updated')
|
||||
|
||||
self.assertEqual(
|
||||
{'slug0': {'oid': self.fid},
|
||||
'slug1': {'oid': self.fid},
|
||||
'slug2': {'oid': self.fid},
|
||||
},
|
||||
node['properties']['attachments'],
|
||||
'The link should have been removed from the attachment')
|
||||
|
||||
|
||||
class ReconcileNodeDurationTest(AbstractPillarTest):
|
||||
def setUp(self, **kwargs):
|
||||
super().setUp(**kwargs)
|
||||
@@ -459,3 +318,52 @@ class DeleteProjectlessFilesTest(AbstractPillarTest):
|
||||
self.assertEqual(file1_doc, found1)
|
||||
self.assertEqual(file3_doc, found3)
|
||||
self.assertEqual(file3_doc, found3)
|
||||
|
||||
|
||||
class FixMissingActivitiesSubscription(AbstractPillarTest):
|
||||
def test_fix_missing_activities_subscription(self):
|
||||
from pillar.cli.maintenance import fix_missing_activities_subscription_defaults
|
||||
|
||||
with self.app.app_context():
|
||||
subscriptions_collection = self.app.db('activities-subscriptions')
|
||||
|
||||
invalid_subscription = {
|
||||
'user': ObjectId(),
|
||||
'context_object_type': 'node',
|
||||
'context_object': ObjectId(),
|
||||
}
|
||||
|
||||
valid_subscription = {
|
||||
'user': ObjectId(),
|
||||
'context_object_type': 'node',
|
||||
'context_object': ObjectId(),
|
||||
'is_subscribed': False,
|
||||
'notifications': {'web': False, }
|
||||
}
|
||||
|
||||
result = subscriptions_collection.insert_one(
|
||||
invalid_subscription,
|
||||
bypass_document_validation=True)
|
||||
|
||||
id_invalid = result.inserted_id
|
||||
|
||||
result = subscriptions_collection.insert_one(
|
||||
valid_subscription,
|
||||
bypass_document_validation=True)
|
||||
|
||||
id_valid = result.inserted_id
|
||||
|
||||
fix_missing_activities_subscription_defaults() # Dry run. Nothing should change
|
||||
invalid_subscription = subscriptions_collection.find_one({'_id': id_invalid})
|
||||
self.assertNotIn('is_subscribed', invalid_subscription.keys())
|
||||
self.assertNotIn('notifications', invalid_subscription.keys())
|
||||
|
||||
fix_missing_activities_subscription_defaults(go=True) # Real run. Invalid should be updated
|
||||
invalid_subscription = subscriptions_collection.find_one({'_id': id_invalid})
|
||||
self.assertTrue(invalid_subscription['is_subscribed'])
|
||||
self.assertTrue(invalid_subscription['notifications']['web'])
|
||||
|
||||
# Was already ok. Should not have been updated
|
||||
valid_subscription = subscriptions_collection.find_one({'_id': id_valid})
|
||||
self.assertFalse(valid_subscription['is_subscribed'])
|
||||
self.assertFalse(valid_subscription['notifications']['web'])
|
||||
|
Reference in New Issue
Block a user