Introducing Pillar Framework

Refactor of pillar-server and pillar-web into a single python package. This
simplifies the overall architecture of pillar applications.

Special thanks @sybren and @venomgfx
This commit is contained in:
2016-08-19 09:19:06 +02:00
parent a5e92e1d87
commit 2c5dc34ea2
232 changed files with 79508 additions and 2232 deletions

View File

@@ -0,0 +1,5 @@
from .routes import blueprint
def setup_app(app, url_prefix=None):
app.register_blueprint(blueprint, url_prefix=url_prefix)

View File

@@ -0,0 +1,2 @@
def append_custom_node_endpoints():
pass

View File

@@ -0,0 +1,189 @@
import logging
from flask import current_app
from flask import request
from flask import jsonify
from flask import render_template
from flask.ext.login import login_required
from flask.ext.login import current_user
from pillarsdk import Node
from pillarsdk import Project
import werkzeug.exceptions as wz_exceptions
from pillar.web.nodes.routes import blueprint
from pillar.web.utils import gravatar
from pillar.web.utils import pretty_date
from pillar.web.utils import system_util
log = logging.getLogger(__name__)
@blueprint.route('/comments/create', methods=['POST'])
@login_required
def comments_create():
content = request.form['content']
parent_id = request.form.get('parent_id')
api = system_util.pillar_api()
parent_node = Node.find(parent_id, api=api)
node_asset_props = dict(
project=parent_node.project,
name='Comment',
user=current_user.objectid,
node_type='comment',
properties=dict(
content=content,
status='published',
confidence=0,
rating_positive=0,
rating_negative=0))
if parent_id:
node_asset_props['parent'] = parent_id
# Get the parent node and check if it's a comment. In which case we flag
# the current comment as a reply.
parent_node = Node.find(parent_id, api=api)
if parent_node.node_type == 'comment':
node_asset_props['properties']['is_reply'] = True
node_asset = Node(node_asset_props)
node_asset.create(api=api)
return jsonify(
asset_id=node_asset._id,
content=node_asset.properties.content)
@blueprint.route('/comments/<string(length=24):comment_id>', methods=['POST'])
@login_required
def comment_edit(comment_id):
"""Allows a user to edit their comment (or any they have PUT access to)."""
api = system_util.pillar_api()
# Fetch the old comment.
comment_node = Node.find(comment_id, api=api)
if comment_node.node_type != 'comment':
log.info('POST to %s node %s done as if it were a comment edit; rejected.',
comment_node.node_type, comment_id)
raise wz_exceptions.BadRequest('Node ID is not a comment.')
# Update the node.
comment_node.properties.content = request.form['content']
update_ok = comment_node.update(api=api)
if not update_ok:
log.warning('Unable to update comment node %s: %s',
comment_id, comment_node.error)
raise wz_exceptions.InternalServerError('Unable to update comment node, unknown why.')
return '', 204
def format_comment(comment, is_reply=False, is_team=False, replies=None):
"""Format a comment node into a simpler dictionary.
:param comment: the comment object
:param is_reply: True if the comment is a reply to another comment
:param is_team: True if the author belongs to the group that owns the node
:param replies: list of replies (formatted with this function)
"""
try:
is_own = (current_user.objectid == comment.user._id) \
if current_user.is_authenticated else False
except AttributeError:
current_app.bugsnag.notify(Exception(
'Missing user for embedded user ObjectId'),
meta_data={'nodes_info': {'node_id': comment['_id']}})
return
is_rated = False
is_rated_positive = None
if comment.properties.ratings:
for rating in comment.properties.ratings:
if current_user.is_authenticated and rating.user == current_user.objectid:
is_rated = True
is_rated_positive = rating.is_positive
break
return dict(_id=comment._id,
gravatar=gravatar(comment.user.email, size=32),
time_published=pretty_date(comment._created, detail=True),
rating=comment.properties.rating_positive - comment.properties.rating_negative,
author=comment.user.full_name,
author_username=comment.user.username,
content=comment.properties.content,
is_reply=is_reply,
is_own=is_own,
is_rated=is_rated,
is_rated_positive=is_rated_positive,
is_team=is_team,
replies=replies)
@blueprint.route("/comments/")
def comments_index():
parent_id = request.args.get('parent_id')
# Get data only if we format it
api = system_util.pillar_api()
if request.args.get('format') == 'json':
nodes = Node.all({
'where': '{"node_type" : "comment", "parent": "%s"}' % (parent_id),
'embedded': '{"user":1}'}, api=api)
comments = []
for comment in nodes._items:
# Query for first level children (comment replies)
replies = Node.all({
'where': '{"node_type" : "comment", "parent": "%s"}' % (comment._id),
'embedded': '{"user":1}'}, api=api)
replies = replies._items if replies._items else None
if replies:
replies = [format_comment(reply, is_reply=True) for reply in replies]
comments.append(
format_comment(comment, is_reply=False, replies=replies))
return_content = jsonify(items=[c for c in comments if c is not None])
else:
parent_node = Node.find(parent_id, api=api)
project = Project({'_id': parent_node.project})
has_method_POST = project.node_type_has_method('comment', 'POST', api=api)
# Data will be requested via javascript
return_content = render_template('nodes/custom/_comments.html',
parent_id=parent_id,
has_method_POST=has_method_POST)
return return_content
@blueprint.route("/comments/<comment_id>/rate/<operation>", methods=['POST'])
@login_required
def comments_rate(comment_id, operation):
"""Comment rating function
:param comment_id: the comment id
:type comment_id: str
:param rating: the rating (is cast from 0 to False and from 1 to True)
:type rating: int
"""
if operation not in {u'revoke', u'upvote', u'downvote'}:
raise wz_exceptions.BadRequest('Invalid operation')
api = system_util.pillar_api()
comment = Node.find(comment_id, {'projection': {'_id': 1}}, api=api)
if not comment:
log.info('Node %i not found; how could someone click on the upvote/downvote button?',
comment_id)
raise wz_exceptions.NotFound()
# PATCH the node and return the result.
result = comment.patch({'op': operation}, api=api)
assert result['_status'] == 'OK'
return jsonify({
'status': 'success',
'data': {
'op': operation,
'rating_positive': result.properties.rating_positive,
'rating_negative': result.properties.rating_negative,
}})

View File

@@ -0,0 +1,36 @@
from flask import request
from flask import jsonify
from flask.ext.login import login_required
from flask.ext.login import current_user
from pillarsdk import Node
from pillar.web.utils import system_util
from ..routes import blueprint
@blueprint.route('/groups/create', methods=['POST'])
@login_required
def groups_create():
# Use current_project_id from the session instead of the cookie
name = request.form['name']
project_id = request.form['project_id']
parent_id = request.form.get('parent_id')
api = system_util.pillar_api()
# We will create the Node object later on, after creating the file object
node_asset_props = dict(
name=name,
user=current_user.objectid,
node_type='group',
project=project_id,
properties=dict(
status='published'))
# Add parent_id only if provided (we do not provide it when creating groups
# at the Project root)
if parent_id:
node_asset_props['parent'] = parent_id
node_asset = Node(node_asset_props)
node_asset.create(api=api)
return jsonify(
status='success',
data=dict(name=name, asset_id=node_asset._id))

View File

@@ -0,0 +1,168 @@
from pillarsdk import Node
from pillarsdk import Project
from pillarsdk.exceptions import ResourceNotFound
from flask import abort
from flask import render_template
from flask import redirect
from flask.ext.login import login_required
from flask.ext.login import current_user
from pillar.web.utils import system_util
from pillar.web.utils import attach_project_pictures
from pillar.web.utils import get_file
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
from pillar.web.projects.routes import project_update_nodes_list
def posts_view(project_id, url=None):
"""View individual blogpost"""
api = system_util.pillar_api()
# Fetch project (for backgroud images and links generation)
project = Project.find(project_id, api=api)
attach_project_pictures(project, api)
try:
blog = Node.find_one({
'where': {'node_type': 'blog', 'project': project_id},
}, api=api)
except ResourceNotFound:
abort(404)
if url:
try:
post = Node.find_one({
'where': '{"parent": "%s", "properties.url": "%s"}' % (blog._id, url),
'embedded': '{"node_type": 1, "user": 1}',
}, api=api)
if post.picture:
post.picture = get_file(post.picture, api=api)
except ResourceNotFound:
return abort(404)
# If post is not published, check that the user is also the author of
# the post. If not, return 404.
if post.properties.status != "published":
if current_user.is_authenticated:
if not post.has_method('PUT'):
abort(403)
else:
abort(403)
return render_template(
'nodes/custom/post/view.html',
blog=blog,
node=post,
project=project,
title='blog',
api=api)
else:
node_type_post = project.get_node_type('post')
status_query = "" if blog.has_method('PUT') else ', "properties.status": "published"'
posts = Node.all({
'where': '{"parent": "%s" %s}' % (blog._id, status_query),
'embedded': '{"user": 1}',
'sort': '-_created'
}, api=api)
for post in posts._items:
post.picture = get_file(post.picture, api=api)
return render_template(
'nodes/custom/blog/index.html',
node_type_post=node_type_post,
posts=posts._items,
project=project,
title='blog',
api=api)
@blueprint.route("/posts/<project_id>/create", methods=['GET', 'POST'])
@login_required
def posts_create(project_id):
api = system_util.pillar_api()
try:
project = Project.find(project_id, api=api)
except ResourceNotFound:
return abort(404)
attach_project_pictures(project, api)
blog = Node.find_one({
'where': {'node_type': 'blog', 'project': project_id}}, api=api)
node_type = project.get_node_type('post')
# Check if user is allowed to create a post in the blog
if not project.node_type_has_method('post', 'POST', api=api):
return abort(403)
form = get_node_form(node_type)
if form.validate_on_submit():
# Create new post object from scratch
post_props = dict(
node_type='post',
name=form.name.data,
picture=form.picture.data,
user=current_user.objectid,
parent=blog._id,
project=project._id,
properties=dict(
content=form.content.data,
status=form.status.data,
url=form.url.data))
if form.picture.data == '':
post_props['picture'] = None
post = Node(post_props)
post.create(api=api)
# Only if the node is set as published, push it to the list
if post.properties.status == 'published':
project_update_nodes_list(post, project_id=project._id, list_name='blog')
return redirect(url_for_node(node=post))
form.parent.data = blog._id
return render_template('nodes/custom/post/create.html',
node_type=node_type,
form=form,
project=project,
api=api)
@blueprint.route("/posts/<post_id>/edit", methods=['GET', 'POST'])
@login_required
def posts_edit(post_id):
api = system_util.pillar_api()
try:
post = Node.find(post_id, {
'embedded': '{"user": 1}'}, api=api)
except ResourceNotFound:
return abort(404)
# Check if user is allowed to edit the post
if not post.has_method('PUT'):
return abort(403)
project = Project.find(post.project, api=api)
attach_project_pictures(project, api)
node_type = project.get_node_type(post.node_type)
form = get_node_form(node_type)
if form.validate_on_submit():
if process_node_form(form, node_id=post_id, node_type=node_type,
user=current_user.objectid):
# The the post is published, add it to the list
if form.status.data == 'published':
project_update_nodes_list(post, project_id=project._id, list_name='blog')
return redirect(url_for_node(node=post))
form.parent.data = post.parent
form.name.data = post.name
form.content.data = post.properties.content
form.status.data = post.properties.status
form.url.data = post.properties.url
if post.picture:
form.picture.data = post.picture
# Embed picture file
post.picture = get_file(post.picture, api=api)
if post.properties.picture_square:
form.picture_square.data = post.properties.picture_square
return render_template('nodes/custom/post/edit.html',
node_type=node_type,
post=post,
form=form,
project=project,
api=api)

View File

@@ -0,0 +1,31 @@
import requests
import os
from pillar.web.utils import system_util
class StorageNode(object):
path = "storage"
def __init__(self, storage_node):
self.storage_node = storage_node
@property
def entrypoint(self):
return os.path.join(
system_util.pillar_server_endpoint(),
self.path,
self.storage_node.properties.backend,
self.storage_node.properties.project,
self.storage_node.properties.subdir)
# @current_app.cache.memoize(timeout=3600)
def browse(self, path=None):
"""Search a storage node for a path, which can point both to a directory
of to a file.
"""
if path is None:
url = self.entrypoint
else:
url = os.path.join(self.entrypoint, path)
r = requests.get(url)
return r.json()

289
pillar/web/nodes/forms.py Normal file
View File

@@ -0,0 +1,289 @@
import logging
from datetime import datetime
from datetime import date
import pillarsdk
from flask import current_app
from flask_wtf import Form
from wtforms import StringField
from wtforms import DateField
from wtforms import SelectField
from wtforms import HiddenField
from wtforms import BooleanField
from wtforms import IntegerField
from wtforms import FloatField
from wtforms import TextAreaField
from wtforms import DateTimeField
from wtforms import SelectMultipleField
from wtforms import FieldList
from wtforms.validators import DataRequired
from pillar.web.utils import system_util
from pillar.web.utils.forms import FileSelectField
from pillar.web.utils.forms import ProceduralFileSelectForm
from pillar.web.utils.forms import CustomFormField
from pillar.web.utils.forms import build_file_select_form
log = logging.getLogger(__name__)
def add_form_properties(form_class, node_schema, form_schema, prefix=''):
"""Add fields to a form based on the node and form schema provided.
:type node_schema: dict
:param node_schema: the validation schema used by Cerberus
:type form_class: class
:param form_class: The form class to which we append fields
:type form_schema: dict
:param form_schema: description of how to build the form (which fields to
show and hide)
"""
for prop, schema_prop in node_schema.iteritems():
form_prop = form_schema.get(prop, {})
if prop == 'items':
continue
if not form_prop.get('visible', True):
continue
prop_name = "{0}{1}".format(prefix, prop)
# Recursive call if detects a dict
field_type = schema_prop['type']
if field_type == 'dict':
# This works if the dictionary schema is hardcoded.
# If we define it using propertyschema and valueschema, this
# validation pattern does not work and crahses.
add_form_properties(form_class, schema_prop['schema'],
form_prop['schema'], "{0}__".format(prop_name))
continue
if field_type == 'list':
if prop == 'attachments':
# class AttachmentForm(Form):
# pass
# AttachmentForm.file = FileSelectField('file')
# AttachmentForm.size = StringField()
# AttachmentForm.slug = StringField()
field = FieldList(CustomFormField(ProceduralFileSelectForm))
elif prop == 'files':
schema = schema_prop['schema']['schema']
file_select_form = build_file_select_form(schema)
field = FieldList(CustomFormField(file_select_form),
min_entries=1)
elif 'allowed' in schema_prop['schema']:
choices = [(c, c) for c in schema_prop['schema']['allowed']]
field = SelectMultipleField(choices=choices)
else:
field = SelectMultipleField(choices=[])
elif 'allowed' in schema_prop:
select = []
for option in schema_prop['allowed']:
select.append((str(option), str(option)))
field = SelectField(choices=select)
elif field_type == 'datetime':
if form_prop.get('dateonly'):
field = DateField(prop_name, default=date.today())
else:
field = DateTimeField(prop_name, default=datetime.now())
elif field_type == 'integer':
field = IntegerField(prop_name, default=0)
elif field_type == 'float':
field = FloatField(prop_name, default=0)
elif field_type == 'boolean':
field = BooleanField(prop_name)
elif field_type == 'objectid' and 'data_relation' in schema_prop:
if schema_prop['data_relation']['resource'] == 'files':
field = FileSelectField(prop_name)
else:
field = StringField(prop_name)
elif schema_prop.get('maxlength', 0) > 64:
field = TextAreaField(prop_name)
else:
field = StringField(prop_name)
setattr(form_class, prop_name, field)
def get_node_form(node_type):
"""Get a procedurally generated WTForm, based on the dyn_schema and
node_schema of a specific node_type.
:type node_type: dict
:param node_type: Describes the node type via dyn_schema, form_schema and
parent
"""
class ProceduralForm(Form):
pass
node_schema = node_type['dyn_schema'].to_dict()
form_prop = node_type['form_schema'].to_dict()
parent_prop = node_type['parent']
ProceduralForm.name = StringField('Name', validators=[DataRequired()])
# Parenting
if parent_prop:
parent_names = ", ".join(parent_prop)
ProceduralForm.parent = HiddenField('Parent ({0})'.format(parent_names))
ProceduralForm.description = TextAreaField('Description')
ProceduralForm.picture = FileSelectField('Picture', file_format='image')
ProceduralForm.node_type = HiddenField(default=node_type['name'])
add_form_properties(ProceduralForm, node_schema, form_prop)
return ProceduralForm()
def recursive(path, rdict, data):
item = path.pop(0)
if not item in rdict:
rdict[item] = {}
if len(path) > 0:
rdict[item] = recursive(path, rdict[item], data)
else:
rdict[item] = data
return rdict
def process_node_form(form, node_id=None, node_type=None, user=None):
"""Generic function used to process new nodes, as well as edits
"""
if not user:
log.warning('process_node_form(node_id=%s) called while user not logged in', node_id)
return False
api = system_util.pillar_api()
node_schema = node_type['dyn_schema'].to_dict()
form_schema = node_type['form_schema'].to_dict()
if node_id:
# Update existing node
node = pillarsdk.Node.find(node_id, api=api)
node.name = form.name.data
node.description = form.description.data
if 'picture' in form:
node.picture = form.picture.data
if node.picture == 'None' or node.picture == '':
node.picture = None
if 'parent' in form:
if form.parent.data != "":
node.parent = form.parent.data
def update_data(node_schema, form_schema, prefix=""):
for pr in node_schema:
schema_prop = node_schema[pr]
form_prop = form_schema.get(pr, {})
if pr == 'items':
continue
if 'visible' in form_prop and not form_prop['visible']:
continue
prop_name = "{0}{1}".format(prefix, pr)
if schema_prop['type'] == 'dict':
update_data(
schema_prop['schema'],
form_prop['schema'],
"{0}__".format(prop_name))
continue
data = form[prop_name].data
if schema_prop['type'] == 'dict':
if data == 'None':
continue
elif schema_prop['type'] == 'integer':
if data == '':
data = 0
else:
data = int(form[prop_name].data)
elif schema_prop['type'] == 'datetime':
data = datetime.strftime(data,
app.config['RFC1123_DATE_FORMAT'])
elif schema_prop['type'] == 'list':
if pr == 'attachments':
# data = json.loads(data)
data = [dict(field='description', files=data)]
elif pr == 'files':
# Only keep those items that actually refer to a file.
data = [file_item for file_item in data
if file_item.get('file')]
# elif pr == 'tags':
# data = [tag.strip() for tag in data.split(',')]
elif schema_prop['type'] == 'objectid':
if data == '':
# Set empty object to None so it gets removed by the
# SDK before node.update()
data = None
else:
if pr in form:
data = form[prop_name].data
path = prop_name.split('__')
if len(path) > 1:
recursive_prop = recursive(
path, node.properties.to_dict(), data)
node.properties = recursive_prop
else:
node.properties[prop_name] = data
update_data(node_schema, form_schema)
ok = node.update(api=api)
if not ok:
log.warning('Unable to update node: %s', node.error)
# if form.picture.data:
# image_data = request.files[form.picture.name].read()
# post = node.replace_picture(image_data, api=api)
return ok
else:
# Create a new node
node = pillarsdk.Node()
prop = {}
files = {}
prop['name'] = form.name.data
prop['description'] = form.description.data
prop['user'] = user
if 'picture' in form:
prop['picture'] = form.picture.data
if prop['picture'] == 'None' or prop['picture'] == '':
prop['picture'] = None
if 'parent' in form:
prop['parent'] = form.parent.data
prop['properties'] = {}
def get_data(node_schema, form_schema, prefix=""):
for pr in node_schema:
schema_prop = node_schema[pr]
form_prop = form_schema.get(pr, {})
if pr == 'items':
continue
if 'visible' in form_prop and not form_prop['visible']:
continue
prop_name = "{0}{1}".format(prefix, pr)
if schema_prop['type'] == 'dict':
get_data(
schema_prop['schema'],
form_prop['schema'],
"{0}__".format(prop_name))
continue
data = form[prop_name].data
if schema_prop['type'] == 'media':
tmpfile = '/tmp/binary_data'
data.save(tmpfile)
binfile = open(tmpfile, 'rb')
files[pr] = binfile
continue
if schema_prop['type'] == 'integer':
if data == '':
data = 0
if schema_prop['type'] == 'list':
if data == '':
data = []
if schema_prop['type'] == 'datetime':
data = datetime.strftime(data, app.config['RFC1123_DATE_FORMAT'])
if schema_prop['type'] == 'objectid':
if data == '':
data = None
path = prop_name.split('__')
if len(path) > 1:
prop['properties'] = recursive(path, prop['properties'], data)
else:
prop['properties'][prop_name] = data
get_data(node_schema, form_schema)
prop['node_type'] = form.node_type_id.data
post = node.post(prop, api=api)
return post

688
pillar/web/nodes/routes.py Normal file
View File

@@ -0,0 +1,688 @@
import os
import json
import logging
from datetime import datetime
import pillarsdk
from pillarsdk import Node
from pillarsdk import Project
from pillarsdk.exceptions import ResourceNotFound
from pillarsdk.exceptions import ForbiddenAccess
from flask import Blueprint, current_app
from flask import redirect
from flask import render_template
from flask import url_for
from flask import request
from flask import jsonify
from flask import abort
from flask_login import current_user
from werkzeug.exceptions import NotFound
from wtforms import SelectMultipleField
from flask.ext.login import login_required
from jinja2.exceptions import TemplateNotFound
from pillar.web.utils import caching
from pillar.web.nodes.forms import get_node_form
from pillar.web.nodes.forms import process_node_form
from pillar.web.nodes.custom.storage import StorageNode
from pillar.web.projects.routes import project_update_nodes_list
from pillar.web.utils import get_file
from pillar.web.utils.jstree import jstree_build_children
from pillar.web.utils.jstree import jstree_build_from_node
from pillar.web.utils.forms import ProceduralFileSelectForm
from pillar.web.utils.forms import build_file_select_form
from pillar.web import system_util
blueprint = Blueprint('nodes', __name__)
log = logging.getLogger(__name__)
def get_node(node_id, user_id):
api = system_util.pillar_api()
node = Node.find(node_id + '/?embedded={"node_type":1}', api=api)
return node.to_dict()
def get_node_children(node_id, node_type_name, user_id):
"""This function is currently unused since it does not give significant
performance improvements.
"""
api = system_util.pillar_api()
if node_type_name == 'group':
published_status = ',"properties.status": "published"'
else:
published_status = ''
children = Node.all({
'where': '{"parent": "%s" %s}' % (node_id, published_status),
'embedded': '{"node_type": 1}'}, api=api)
return children.to_dict()
@blueprint.route("/<node_id>/jstree")
def jstree(node_id):
"""JsTree view.
This return a lightweight version of the node, to be used by JsTree in the
frontend. We have two possible cases:
- https://pillar/<node_id>/jstree (construct the whole
expanded tree starting from the node_id. Use only once)
- https://pillar/<node_id>/jstree&children=1 (deliver the
children of a node - use in the navigation of the tree)
"""
# Get node with basic embedded data
api = system_util.pillar_api()
node = Node.find(node_id, {
'projection': {
'name': 1,
'node_type': 1,
'parent': 1,
'project': 1,
'properties.content_type': 1,
}
}, api=api)
if request.args.get('children') != '1':
return jsonify(items=jstree_build_from_node(node))
if node.node_type == 'storage':
storage = StorageNode(node)
# Check if we specify a path within the storage
path = request.args.get('path')
# Generate the storage listing
listing = storage.browse(path)
# Inject the current node id in the response, so that JsTree can
# expose the storage_node property and use it for further queries
listing['storage_node'] = node._id
if 'children' in listing:
for child in listing['children']:
child['storage_node'] = node._id
return jsonify(listing)
return jsonify(jstree_build_children(node))
@blueprint.route("/<node_id>/view")
def view(node_id):
api = system_util.pillar_api()
# Get node, we'll embed linked objects later.
try:
node = Node.find(node_id, api=api)
except ResourceNotFound:
return render_template('errors/404_embed.html')
except ForbiddenAccess:
return render_template('errors/403_embed.html')
node_type_name = node.node_type
if node_type_name == 'post':
# Posts shouldn't be shown at this route, redirect to the correct one.
return redirect(url_for_node(node=node))
# Set the default name of the template path based on the node name
template_path = os.path.join('nodes', 'custom', node_type_name)
# Set the default action for a template. By default is view and we override
# it only if we are working storage nodes, where an 'index' is also possible
template_action = 'view'
def allow_link():
"""Helper function to cross check if the user is authenticated, and it
is has the 'subscriber' role. Also, we check if the node has world GET
permissions, which means it's free.
"""
# Check if node permissions for the world exist (if node is free)
if node.permissions and node.permissions.world:
return 'GET' in node.permissions.world
if current_user.is_authenticated:
allowed_roles = {u'subscriber', u'demo', u'admin'}
return bool(allowed_roles.intersection(current_user.roles or ()))
return False
link_allowed = allow_link()
node_type_handlers = {
'asset': _view_handler_asset,
'storage': _view_handler_storage,
'texture': _view_handler_texture,
'hdri': _view_handler_hdri,
}
if node_type_name in node_type_handlers:
handler = node_type_handlers[node_type_name]
template_path, template_action = handler(node, template_path, template_action, link_allowed)
# Fetch linked resources.
node.picture = get_file(node.picture, api=api)
node.user = node.user and pillarsdk.User.find(node.user, api=api)
try:
node.parent = node.parent and pillarsdk.Node.find(node.parent, api=api)
except ForbiddenAccess:
# This can happen when a node has world-GET, but the parent doesn't.
node.parent = None
# Get children
children_projection = {'project': 1, 'name': 1, 'picture': 1, 'parent': 1,
'node_type': 1, 'properties.order': 1, 'properties.status': 1,
'user': 1, 'properties.content_type': 1}
children_where = {'parent': node._id}
if node_type_name == 'group':
children_where['properties.status'] = 'published'
children_projection['permissions.world'] = 1
else:
children_projection['properties.files'] = 1
children_projection['properties.is_tileable'] = 1
try:
children = Node.all({
'projection': children_projection,
'where': children_where,
'sort': [('properties.order', 1), ('name', 1)]}, api=api)
except ForbiddenAccess:
return render_template('errors/403_embed.html')
children = children._items
for child in children:
child.picture = get_file(child.picture, api=api)
if request.args.get('format') == 'json':
node = node.to_dict()
node['url_edit'] = url_for('nodes.edit', node_id=node['_id'])
return jsonify({
'node': node,
'children': children.to_dict() if children else {},
'parent': node['parent'] if 'parent' in node else {}
})
if 't' in request.args:
template_path = os.path.join('nodes', 'custom', 'asset')
template_action = 'view_theatre'
template_path = '{0}/{1}_embed.html'.format(template_path, template_action)
# template_path_full = os.path.join(current_app.config['TEMPLATES_PATH'], template_path)
#
# # Check if template exists on the filesystem
# if not os.path.exists(template_path_full):
# log.warning('Template %s does not exist for node type %s',
# template_path, node_type_name)
# raise NotFound("Missing template '{0}'".format(template_path))
return render_template(template_path,
node_id=node._id,
node=node,
parent=node.parent,
children=children,
config=current_app.config,
api=api)
def _view_handler_asset(node, template_path, template_action, link_allowed):
# Attach the file document to the asset node
node_file = get_file(node.properties.file)
node.file = node_file
# Remove the link to the file if it's not allowed.
if node_file and not link_allowed:
node.file.link = None
if node_file and node_file.content_type is not None:
asset_type = node_file.content_type.split('/')[0]
else:
asset_type = None
if asset_type == 'video':
# Process video type and select video template
if link_allowed:
sources = []
if node_file and node_file.variations:
for f in node_file.variations:
sources.append({'type': f.content_type, 'src': f.link})
# Build a link that triggers download with proper filename
# TODO: move this to Pillar
if f.backend == 'cdnsun':
f.link = "{0}&name={1}.{2}".format(f.link, node.name, f.format)
node.video_sources = json.dumps(sources)
node.file_variations = node_file.variations
else:
node.video_sources = None
node.file_variations = None
elif asset_type != 'image':
# Treat it as normal file (zip, blend, application, etc)
asset_type = 'file'
template_path = os.path.join(template_path, asset_type)
return template_path, template_action
def _view_handler_storage(node, template_path, template_action, link_allowed):
storage = StorageNode(node)
path = request.args.get('path')
listing = storage.browse(path)
node.name = listing['name']
listing['storage_node'] = node._id
# If the item has children we are working with a group
if 'children' in listing:
for child in listing['children']:
child['storage_node'] = node._id
child['name'] = child['text']
child['content_type'] = os.path.dirname(child['type'])
node.children = listing['children']
template_action = 'index'
else:
node.status = 'published'
node.length = listing['size']
node.download_link = listing['signed_url']
return template_path, template_action
def _view_handler_texture(node, template_path, template_action, link_allowed):
for f in node.properties.files:
f.file = get_file(f.file)
# Remove the link to the file if it's not allowed.
if f.file and not link_allowed:
f.file.link = None
return template_path, template_action
def _view_handler_hdri(node, template_path, template_action, link_allowed):
if not link_allowed:
node.properties.files = None
else:
for f in node.properties.files:
f.file = get_file(f.file)
return template_path, template_action
@blueprint.route("/<node_id>/edit", methods=['GET', 'POST'])
@login_required
def edit(node_id):
"""Generic node editing form
"""
def set_properties(dyn_schema, form_schema, node_properties, form,
prefix="",
set_data=True):
"""Initialize custom properties for the form. We run this function once
before validating the function with set_data=False, so that we can set
any multiselect field that was originally specified empty and fill it
with the current choices.
"""
for prop in dyn_schema:
schema_prop = dyn_schema[prop]
form_prop = form_schema.get(prop, {})
prop_name = "{0}{1}".format(prefix, prop)
if schema_prop['type'] == 'dict':
set_properties(
schema_prop['schema'],
form_prop['schema'],
node_properties[prop_name],
form,
"{0}__".format(prop_name))
continue
if prop_name not in form:
continue
try:
db_prop_value = node_properties[prop]
except KeyError:
log.debug('%s not found in form for node %s', prop_name, node_id)
continue
if schema_prop['type'] == 'datetime':
db_prop_value = datetime.strptime(db_prop_value,
current_app.config['RFC1123_DATE_FORMAT'])
if isinstance(form[prop_name], SelectMultipleField):
# If we are dealing with a multiselect field, check if
# it's empty (usually because we can't query the whole
# database to pick all the choices). If it's empty we
# populate the choices with the actual data.
if not form[prop_name].choices:
form[prop_name].choices = [(d, d) for d in db_prop_value]
# Choices should be a tuple with value and name
# Assign data to the field
if set_data:
if prop_name == 'attachments':
for attachment_collection in db_prop_value:
for a in attachment_collection['files']:
attachment_form = ProceduralFileSelectForm()
attachment_form.file = a['file']
attachment_form.slug = a['slug']
attachment_form.size = 'm'
form[prop_name].append_entry(attachment_form)
elif prop_name == 'files':
schema = schema_prop['schema']['schema']
# Extra entries are caused by min_entries=1 in the form
# creation.
field_list = form[prop_name]
if len(db_prop_value) > 0:
while len(field_list):
field_list.pop_entry()
for file_data in db_prop_value:
file_form_class = build_file_select_form(schema)
subform = file_form_class()
for key, value in file_data.iteritems():
setattr(subform, key, value)
field_list.append_entry(subform)
# elif prop_name == 'tags':
# form[prop_name].data = ', '.join(data)
else:
form[prop_name].data = db_prop_value
else:
# Default population of multiple file form list (only if
# we are getting the form)
if request.method == 'POST':
continue
if prop_name == 'attachments':
if not db_prop_value:
attachment_form = ProceduralFileSelectForm()
attachment_form.file = 'file'
attachment_form.slug = ''
attachment_form.size = ''
form[prop_name].append_entry(attachment_form)
api = system_util.pillar_api()
node = Node.find(node_id, api=api)
project = Project.find(node.project, api=api)
node_type = project.get_node_type(node.node_type)
form = get_node_form(node_type)
user_id = current_user.objectid
dyn_schema = node_type['dyn_schema'].to_dict()
form_schema = node_type['form_schema'].to_dict()
error = ""
node_properties = node.properties.to_dict()
ensure_lists_exist_as_empty(node.to_dict(), node_type)
set_properties(dyn_schema, form_schema, node_properties, form,
set_data=False)
if form.validate_on_submit():
if process_node_form(form, node_id=node_id, node_type=node_type, user=user_id):
# Handle the specific case of a blog post
if node_type.name == 'post':
project_update_nodes_list(node, list_name='blog')
else:
project_update_nodes_list(node)
# Emergency hardcore cache flush
# cache.clear()
return redirect(url_for('nodes.view', node_id=node_id, embed=1,
_external=True,
_scheme=current_app.config['SCHEME']))
else:
log.debug('Error sending data to Pillar, see Pillar logs.')
error = 'Server error'
else:
if form.errors:
log.debug('Form errors: %s', form.errors)
# Populate Form
form.name.data = node.name
form.description.data = node.description
if 'picture' in form:
form.picture.data = node.picture
if node.parent:
form.parent.data = node.parent
set_properties(dyn_schema, form_schema, node_properties, form)
# Get previews
node.picture = get_file(node.picture, api=api) if node.picture else None
# Get Parent
try:
parent = Node.find(node['parent'], api=api)
except KeyError:
parent = None
except ResourceNotFound:
parent = None
embed_string = ''
# Check if we want to embed the content via an AJAX call
if request.args.get('embed'):
if request.args.get('embed') == '1':
# Define the prefix for the embedded template
embed_string = '_embed'
template = '{0}/edit{1}.html'.format(node_type['name'], embed_string)
# We should more simply check if the template file actually exsists on
# the filesystem level
try:
return render_template(
template,
node=node,
parent=parent,
form=form,
errors=form.errors,
error=error,
api=api)
except TemplateNotFound:
template = 'nodes/edit{1}.html'.format(node_type['name'], embed_string)
return render_template(
template,
node=node,
parent=parent,
form=form,
errors=form.errors,
error=error,
api=api)
def ensure_lists_exist_as_empty(node_doc, node_type):
"""Ensures that any properties of type 'list' exist as empty lists.
This allows us to iterate over lists without worrying that they
are set to None. Only works for top-level list properties.
"""
node_properties = node_doc.setdefault('properties', {})
for prop, schema in node_type.dyn_schema.to_dict().iteritems():
if schema['type'] != 'list':
continue
if node_properties.get(prop) is None:
node_properties[prop] = []
@blueprint.route('/create', methods=['POST'])
@login_required
def create():
"""Create a node. Requires a number of params:
- project id
- node_type
- parent node (optional)
"""
if request.method != 'POST':
return abort(403)
project_id = request.form['project_id']
parent_id = request.form.get('parent_id')
node_type_name = request.form['node_type_name']
api = system_util.pillar_api()
# Fetch the Project or 404
try:
project = Project.find(project_id, api=api)
except ResourceNotFound:
return abort(404)
node_type = project.get_node_type(node_type_name)
node_type_name = 'folder' if node_type['name'] == 'group' else \
node_type['name']
node_props = dict(
name='New {}'.format(node_type_name),
project=project['_id'],
user=current_user.objectid,
node_type=node_type['name'],
properties={}
)
if parent_id:
node_props['parent'] = parent_id
ensure_lists_exist_as_empty(node_props, node_type)
node = Node(node_props)
node.create(api=api)
return jsonify(status='success', data=dict(asset_id=node['_id']))
@blueprint.route("/<node_id>/redir")
def redirect_to_context(node_id):
"""Redirects to the context URL of the node.
Comment: redirects to whatever the comment is attached to + #node_id
(unless 'whatever the comment is attached to' already contains '#', then
'#node_id' isn't appended)
Post: redirects to main or project-specific blog post
Other: redirects to project.url + #node_id
"""
if node_id.lower() == '{{objectid}}':
log.warning("JavaScript should have filled in the ObjectID placeholder, but didn't. "
"URL=%s and referrer=%s",
request.url, request.referrer)
raise NotFound('Invalid ObjectID')
try:
url = url_for_node(node_id)
except ValueError as ex:
log.warning("%s: URL=%s and referrer=%s",
str(ex), request.url, request.referrer)
raise NotFound('Invalid ObjectID')
return redirect(url)
def url_for_node(node_id=None, node=None):
assert isinstance(node_id, (basestring, type(None)))
api = system_util.pillar_api()
# Find node by its ID, or the ID by the node, depending on what was passed
# as parameters.
if node is None:
try:
node = Node.find(node_id, api=api)
except ResourceNotFound:
log.warning(
'url_for_node(node_id=%r, node=None): Unable to find node.',
node_id)
raise ValueError('Unable to find node %r' % node_id)
elif node_id is None:
node_id = node['_id']
else:
raise ValueError('Either node or node_id must be given')
return _find_url_for_node(node_id, node=node)
@caching.cache_for_request()
def project_url(project_id, project):
"""Returns the project, raising a ValueError if it can't be found.
Uses the "urler" service endpoint.
"""
if project is not None:
return project
urler_api = system_util.pillar_api(
token=current_app.config['URLER_SERVICE_AUTH_TOKEN'])
return Project.find_from_endpoint(
'/service/urler/%s' % project_id, api=urler_api)
# Cache the actual URL based on the node ID, for the duration of the request.
@caching.cache_for_request()
def _find_url_for_node(node_id, node):
api = system_util.pillar_api()
# Find the node's project, or its ID, depending on whether a project
# was embedded. This is needed in two of the three finder functions.
project_id = node.project
if isinstance(project_id, pillarsdk.Resource):
# Embedded project
project = project_id
project_id = project['_id']
else:
project = None
def find_for_comment():
"""Returns the URL for a comment."""
parent = node
while parent.node_type == 'comment':
if isinstance(parent.parent, pillarsdk.Resource):
parent = parent.parent
continue
try:
parent = Node.find(parent.parent, api=api)
except ResourceNotFound:
log.warning(
'url_for_node(node_id=%r): Unable to find parent node %r',
node_id, parent.parent)
raise ValueError('Unable to find parent node %r' % parent.parent)
# Find the redirection URL for the parent node.
parent_url = url_for_node(node=parent)
if '#' in parent_url:
# We can't attach yet another fragment, so just don't link to
# the comment for now.
return parent_url
return parent_url + '#{}'.format(node_id)
def find_for_post():
"""Returns the URL for a blog post."""
if str(project_id) == current_app.config['MAIN_PROJECT_ID']:
return url_for('main.main_blog',
url=node.properties.url)
the_project = project_url(project_id, project=project)
return url_for('main.project_blog',
project_url=the_project.url,
url=node.properties.url)
# Fallback: Assets, textures, and other node types.
def find_for_other():
the_project = project_url(project_id, project=project)
return url_for('projects.view_node',
project_url=the_project.url,
node_id=node_id)
# Determine which function to use to find the correct URL.
url_finders = {
'comment': find_for_comment,
'post': find_for_post,
}
finder = url_finders.get(node.node_type, find_for_other)
return finder()
# Import of custom modules (using the same nodes decorator)
import custom.comments
import custom.groups
import custom.storage
import custom.posts