diff --git a/pillar/api/node_types/__init__.py b/pillar/api/node_types/__init__.py index cd66b2e0..d75d7d9c 100644 --- a/pillar/api/node_types/__init__.py +++ b/pillar/api/node_types/__init__.py @@ -7,12 +7,14 @@ _file_embedded_schema = { } } +ATTACHMENT_SLUG_REGEX = '[a-zA-Z0-9_ ]+' + _attachments_embedded_schema = { 'type': 'dict', # TODO: will be renamed to 'keyschema' in Cerberus 1.0 'propertyschema': { 'type': 'string', - 'regex': '^[a-zA-Z0-9_ ]+$', + 'regex': '^%s$' % ATTACHMENT_SLUG_REGEX, }, 'valueschema': { 'type': 'dict', diff --git a/pillar/cli.py b/pillar/cli.py index 28dd7fb0..a7c3b52e 100644 --- a/pillar/cli.py +++ b/pillar/cli.py @@ -691,7 +691,7 @@ def upgrade_attachment_schema(proj_url=None, all_projects=False): nodes = nodes_coll.find({ 'project': project['_id'], 'node_type': {'$in': list(node_type_names)}, - 'properties.attachments.0': {'$exists': True}, + 'properties.attachments': {'$exists': True}, }) for node in nodes: log.info(' - Updating schema on node %s (%s)', node['_id'], node.get('name')) diff --git a/pillar/web/nodes/attachments.py b/pillar/web/nodes/attachments.py new file mode 100644 index 00000000..5b57f178 --- /dev/null +++ b/pillar/web/nodes/attachments.py @@ -0,0 +1,88 @@ +import logging +import re + +from bson import ObjectId +import flask +import pillarsdk + +from pillar.api.node_types import ATTACHMENT_SLUG_REGEX +from pillar.web.utils import system_util + +shortcode_re = re.compile(r'@\[(%s)\]' % ATTACHMENT_SLUG_REGEX) +log = logging.getLogger(__name__) + + +def render_attachments(node, field_value): + """Renders attachments referenced in the field value. + + Returns the rendered field. + """ + + # TODO: cache this based on the node's etag and attachment links expiry. + + node_attachments = node[u'properties'][u'attachments'] + if isinstance(node_attachments, list): + log.warning('Old-style attachments property found on node %s. Ignoring them, ' + 'will result in attachments not being found.', node[u'_id']) + return field_value + + if not node_attachments: + return field_value + + def replace(match): + slug = match.group(1) + + try: + att = node_attachments[slug] + except KeyError: + return u'[attachment "%s" not found]' % slug + + return render_attachment(att) + + return shortcode_re.sub(replace, field_value) + + +def render_attachment(attachment): + """Renders an attachment as HTML""" + + oid = ObjectId(attachment[u'oid']) + collection = attachment.collection or u'files' + + renderers = { + 'files': render_attachment_file + } + + try: + renderer = renderers[collection] + except KeyError: + log.error(u'Unable to render attachment from collection %s', collection) + return u'Unable to render attachment' + + return renderer(oid) + + +def render_attachment_file(oid): + """Renders a file attachment.""" + + api = system_util.pillar_api() + sdk_file = pillarsdk.File.find(oid, api=api) + + file_renderers = { + 'image': render_attachment_file_image + } + + mime_type_cat, _ = sdk_file.content_type.split('/', 1) + try: + renderer = file_renderers[mime_type_cat] + except KeyError: + return flask.render_template('nodes/attachments/file_generic.html', file=sdk_file) + + return renderer(sdk_file) + + +def render_attachment_file_image(sdk_file): + """Renders an image file.""" + + variations = {var.size: var for var in sdk_file.variations} + return flask.render_template('nodes/attachments/file_image.html', + file=sdk_file, vars=variations) diff --git a/pillar/web/nodes/custom/posts.py b/pillar/web/nodes/custom/posts.py index 1b0845d9..37d8c921 100644 --- a/pillar/web/nodes/custom/posts.py +++ b/pillar/web/nodes/custom/posts.py @@ -14,6 +14,7 @@ from pillar.web.nodes.routes import blueprint from pillar.web.nodes.routes import url_for_node from pillar.web.nodes.forms import get_node_form from pillar.web.nodes.forms import process_node_form +import pillar.web.nodes.attachments from pillar.web.projects.routes import project_update_nodes_list @@ -53,6 +54,9 @@ def posts_view(project_id=None, project_url=None, url=None): if not (current_user.is_authenticated and post.has_method('PUT')): abort(403) + post['properties']['content'] = pillar.web.nodes.attachments.render_attachments( + post, post['properties']['content']) + return render_template( 'nodes/custom/post/view.html', blog=blog, @@ -72,6 +76,9 @@ def posts_view(project_id=None, project_url=None, url=None): for post in posts._items: post.picture = get_file(post.picture, api=api) + post['properties']['content'] = pillar.web.nodes.attachments.render_attachments( + post, post['properties']['content']) + return render_template( 'nodes/custom/blog/index.html', node_type_post=node_type_post, diff --git a/pillar/web/nodes/routes.py b/pillar/web/nodes/routes.py index 998618f9..678eec3e 100644 --- a/pillar/web/nodes/routes.py +++ b/pillar/web/nodes/routes.py @@ -22,6 +22,7 @@ from wtforms import SelectMultipleField from flask_login import login_required from jinja2.exceptions import TemplateNotFound +import pillar.web.nodes.attachments from pillar.web.utils import caching from pillar.web.nodes.forms import get_node_form from pillar.web.nodes.forms import process_node_form @@ -260,6 +261,8 @@ def _view_handler_asset(node, template_path, template_action, link_allowed): # Treat it as normal file (zip, blend, application, etc) asset_type = 'file' + node['description'] = pillar.web.nodes.attachments.render_attachments(node, node['description']) + template_path = os.path.join(template_path, asset_type) return template_path, template_action diff --git a/src/templates/nodes/attachments/file_generic.jade b/src/templates/nodes/attachments/file_generic.jade new file mode 100644 index 00000000..1c4b101a --- /dev/null +++ b/src/templates/nodes/attachments/file_generic.jade @@ -0,0 +1 @@ +a.attachment(href="{{ file.link }}") {{ file.filename }} diff --git a/src/templates/nodes/attachments/file_image.jade b/src/templates/nodes/attachments/file_image.jade new file mode 100644 index 00000000..aa64479f --- /dev/null +++ b/src/templates/nodes/attachments/file_image.jade @@ -0,0 +1 @@ +img(src="{{ vars['l'].link }}",alt="{{ file.filename }}")