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:
5
pillar/web/nodes/__init__.py
Normal file
5
pillar/web/nodes/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .routes import blueprint
|
||||
|
||||
|
||||
def setup_app(app, url_prefix=None):
|
||||
app.register_blueprint(blueprint, url_prefix=url_prefix)
|
2
pillar/web/nodes/custom/__init__.py
Normal file
2
pillar/web/nodes/custom/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
def append_custom_node_endpoints():
|
||||
pass
|
189
pillar/web/nodes/custom/comments.py
Normal file
189
pillar/web/nodes/custom/comments.py
Normal 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,
|
||||
}})
|
36
pillar/web/nodes/custom/groups.py
Normal file
36
pillar/web/nodes/custom/groups.py
Normal 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))
|
168
pillar/web/nodes/custom/posts.py
Normal file
168
pillar/web/nodes/custom/posts.py
Normal 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)
|
31
pillar/web/nodes/custom/storage.py
Normal file
31
pillar/web/nodes/custom/storage.py
Normal 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
289
pillar/web/nodes/forms.py
Normal 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
688
pillar/web/nodes/routes.py
Normal 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
|
Reference in New Issue
Block a user