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:
2019-02-19 14:16:28 +01:00
parent 250c7e2631
commit 32e25ce129
14 changed files with 424 additions and 427 deletions

View File

@@ -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)

View 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'])

View File

@@ -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'])