diff --git a/pillar/application/__init__.py b/pillar/application/__init__.py index 42a96870..55d91bf5 100644 --- a/pillar/application/__init__.py +++ b/pillar/application/__init__.py @@ -159,11 +159,7 @@ else: from utils.authentication import validate_token from utils.authorization import check_permissions -from utils.gcs import update_file_name -from utils.activities import activity_subscribe -from utils.activities import activity_object_add from utils.activities import notification_parse -from modules import file_storage from modules.projects import before_inserting_projects from modules.projects import after_inserting_projects @@ -183,146 +179,6 @@ def before_returning_resource_permissions(response): check_permissions(item, 'GET', append_allowed_methods=True) -def before_replacing_node(item, original): - check_permissions(original, 'PUT') - update_file_name(item) - - -def after_replacing_node(item, original): - """Push an update to the Algolia index when a node item is updated. If the - project is private, prevent public indexing. - """ - projects_collection = app.data.driver.db['projects'] - project = projects_collection.find_one({'_id': item['project']}, - {'is_private': 1}) - if 'is_private' in project and project['is_private']: - # Skip index updating and return - return - - from algoliasearch.client import AlgoliaException - from utils.algolia import algolia_index_node_save - - try: - algolia_index_node_save(item) - except AlgoliaException as ex: - log.warning('Unable to push node info to Algolia for node %s; %s', - item.get('_id'), ex) - - -def before_inserting_nodes(items): - """Before inserting a node in the collection we check if the user is allowed - and we append the project id to it. - """ - nodes_collection = app.data.driver.db['nodes'] - - def find_parent_project(node): - """Recursive function that finds the ultimate parent of a node.""" - if node and 'parent' in node: - parent = nodes_collection.find_one({'_id': node['parent']}) - return find_parent_project(parent) - if node: - return node - else: - return None - - for item in items: - check_permissions(item, 'POST') - if 'parent' in item and 'project' not in item: - parent = nodes_collection.find_one({'_id': item['parent']}) - project = find_parent_project(parent) - if project: - item['project'] = project['_id'] - - -def after_inserting_nodes(items): - for item in items: - # Skip subscriptions for first level items (since the context is not a - # node, but a project). - # TODO: support should be added for mixed context - if 'parent' not in item: - return - context_object_id = item['parent'] - if item['node_type'] == 'comment': - nodes_collection = app.data.driver.db['nodes'] - parent = nodes_collection.find_one({'_id': item['parent']}) - # Always subscribe to the parent node - activity_subscribe(item['user'], 'node', item['parent']) - if parent['node_type'] == 'comment': - # If the parent is a comment, we provide its own parent as - # context. We do this in order to point the user to an asset - # or group when viewing the notification. - verb = 'replied' - context_object_id = parent['parent'] - # Subscribe to the parent of the parent comment (post or group) - activity_subscribe(item['user'], 'node', parent['parent']) - else: - activity_subscribe(item['user'], 'node', item['_id']) - verb = 'commented' - else: - verb = 'posted' - activity_subscribe(item['user'], 'node', item['_id']) - - activity_object_add( - item['user'], - verb, - 'node', - item['_id'], - 'node', - context_object_id - ) - - -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 - variation, get the link and build a Markdown representation. Search in the - 'field' specified in the attachment and replace the 'slug' tag with the - generated link. - """ - if 'properties' in response and 'attachments' in response['properties']: - files_collection = app.data.driver.db['files'] - for field in response['properties']['attachments']: - for attachment in response['properties']['attachments']: - # Make a list from the property path - field_name_path = attachment['field'].split('.') - # This currently allow to access only properties inside of - # the properties property - if len(field_name_path) > 1: - field_content = response[field_name_path[0]][field_name_path[1]] - # This is for the "normal" first level property - else: - field_content = response[field_name_path[0]] - for af in attachment['files']: - slug = af['slug'] - slug_tag = "[{0}]".format(slug) - f = files_collection.find_one({'_id': ObjectId(af['file'])}) - if f is None: - af['file'] = None - continue - size = f['size'] if 'size' in f else 'l' - # Get the correct variation from the file - thumbnail = next((item for item in f['variations'] if - item['size'] == size), None) - l = file_storage.generate_link(f['backend'], thumbnail['file_path'], - str(f['project'])) - # Build Markdown img string - l = '![{0}]({1} "{2}")'.format(slug, l, f['name']) - # Parse the content of the file and replace the attachment - # tag with the actual image link - field_content = field_content.replace(slug_tag, l) - # Apply the parsed value back to the property. See above for - # clarifications on how this is done. - if len(field_name_path) > 1: - response[field_name_path[0]][field_name_path[1]] = field_content - else: - response[field_name_path[0]] = field_content - - -def resource_parse_attachments(response): - for item in response['_items']: - item_parse_attachments(item) - - def project_node_type_has_method(response): """Check for a specific request arg, and check generate the allowed_methods list for the required node_type. @@ -354,33 +210,26 @@ def before_returning_resource_notifications(response): 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_item_notifications += before_returning_item_notifications app.on_fetched_resource_notifications += before_returning_resource_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 -file_storage.setup_app(app, url_prefix='/storage') # The encoding module (receive notification and report progress) from modules.encoding import encoding from modules.blender_id import blender_id from modules import projects from modules import local_auth +from modules import file_storage from modules import users +from modules import nodes app.register_blueprint(encoding, url_prefix='/encoding') app.register_blueprint(blender_id, url_prefix='/blender_id') projects.setup_app(app, url_prefix='/p') local_auth.setup_app(app, url_prefix='/auth') +file_storage.setup_app(app, url_prefix='/storage') users.setup_app(app) +nodes.setup_app(app) diff --git a/pillar/application/modules/nodes.py b/pillar/application/modules/nodes.py new file mode 100644 index 00000000..b04b7454 --- /dev/null +++ b/pillar/application/modules/nodes.py @@ -0,0 +1,168 @@ +import logging + +from bson import ObjectId +from flask import current_app + +from application.modules import file_storage +from application.utils.authorization import check_permissions +from application.utils.gcs import update_file_name +from application.utils.activities import activity_subscribe, activity_object_add + +log = logging.getLogger(__name__) + + +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 + variation, get the link and build a Markdown representation. Search in the + 'field' specified in the attachment and replace the 'slug' tag with the + generated link. + """ + + if 'properties' not in response or 'attachments' not in response['properties']: + return + + files_collection = current_app.data.driver.db['files'] + for attachment in response['properties']['attachments']: + # Make a list from the property path + field_name_path = attachment['field'].split('.') + # This currently allow to access only properties inside of + # the properties property + if len(field_name_path) > 1: + field_content = response[field_name_path[0]][field_name_path[1]] + # This is for the "normal" first level property + else: + field_content = response[field_name_path[0]] + for af in attachment['files']: + slug = af['slug'] + slug_tag = "[{0}]".format(slug) + f = files_collection.find_one({'_id': ObjectId(af['file'])}) + if f is None: + af['file'] = None + continue + size = f['size'] if 'size' in f else 'l' + # Get the correct variation from the file + thumbnail = next((item for item in f['variations'] if + item['size'] == size), None) + l = file_storage.generate_link(f['backend'], thumbnail['file_path'], + str(f['project'])) + # Build Markdown img string + l = '![{0}]({1} "{2}")'.format(slug, l, f['name']) + # Parse the content of the file and replace the attachment + # tag with the actual image link + field_content = field_content.replace(slug_tag, l) + + # Apply the parsed value back to the property. See above for + # clarifications on how this is done. + if len(field_name_path) > 1: + response[field_name_path[0]][field_name_path[1]] = field_content + else: + response[field_name_path[0]] = field_content + + +def resource_parse_attachments(response): + for item in response['_items']: + item_parse_attachments(item) + +def before_replacing_node(item, original): + check_permissions(original, 'PUT') + update_file_name(item) + + +def after_replacing_node(item, original): + """Push an update to the Algolia index when a node item is updated. If the + project is private, prevent public indexing. + """ + projects_collection = current_app.data.driver.db['projects'] + project = projects_collection.find_one({'_id': item['project']}, + {'is_private': 1}) + if 'is_private' in project and project['is_private']: + # Skip index updating and return + return + + from algoliasearch.client import AlgoliaException + from application.utils.algolia import algolia_index_node_save + + try: + algolia_index_node_save(item) + except AlgoliaException as ex: + log.warning('Unable to push node info to Algolia for node %s; %s', + item.get('_id'), ex) + + +def before_inserting_nodes(items): + """Before inserting a node in the collection we check if the user is allowed + and we append the project id to it. + """ + nodes_collection = current_app.data.driver.db['nodes'] + + def find_parent_project(node): + """Recursive function that finds the ultimate parent of a node.""" + if node and 'parent' in node: + parent = nodes_collection.find_one({'_id': node['parent']}) + return find_parent_project(parent) + if node: + return node + else: + return None + + for item in items: + check_permissions(item, 'POST') + if 'parent' in item and 'project' not in item: + parent = nodes_collection.find_one({'_id': item['parent']}) + project = find_parent_project(parent) + if project: + item['project'] = project['_id'] + + +def after_inserting_nodes(items): + for item in items: + # Skip subscriptions for first level items (since the context is not a + # node, but a project). + # TODO: support should be added for mixed context + if 'parent' not in item: + return + context_object_id = item['parent'] + if item['node_type'] == 'comment': + nodes_collection = current_app.data.driver.db['nodes'] + parent = nodes_collection.find_one({'_id': item['parent']}) + # Always subscribe to the parent node + activity_subscribe(item['user'], 'node', item['parent']) + if parent['node_type'] == 'comment': + # If the parent is a comment, we provide its own parent as + # context. We do this in order to point the user to an asset + # or group when viewing the notification. + verb = 'replied' + context_object_id = parent['parent'] + # Subscribe to the parent of the parent comment (post or group) + activity_subscribe(item['user'], 'node', parent['parent']) + else: + activity_subscribe(item['user'], 'node', item['_id']) + verb = 'commented' + else: + verb = 'posted' + activity_subscribe(item['user'], 'node', item['_id']) + + activity_object_add( + item['user'], + verb, + 'node', + item['_id'], + 'node', + context_object_id + ) + + +def setup_app(app): + from application import before_returning_item_permissions, before_returning_resource_permissions + + # Permission hooks + app.on_fetched_item_nodes += before_returning_item_permissions + app.on_fetched_resource_nodes += before_returning_resource_permissions + + app.on_fetched_item_nodes += item_parse_attachments + app.on_fetched_resource_nodes += resource_parse_attachments + 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