2017-12-22 16:27:05 +01:00
|
|
|
import datetime
|
2016-08-19 09:19:06 +02:00
|
|
|
import json
|
|
|
|
import logging
|
2016-11-24 18:13:46 +01:00
|
|
|
import itertools
|
2017-05-31 10:33:24 +02:00
|
|
|
import typing
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
from pillarsdk import Node
|
|
|
|
from pillarsdk import Project
|
|
|
|
from pillarsdk.exceptions import ResourceNotFound
|
|
|
|
from pillarsdk.exceptions import ForbiddenAccess
|
2018-02-13 14:36:16 +01:00
|
|
|
|
2017-05-24 15:43:34 +02:00
|
|
|
from flask import Blueprint
|
2016-08-19 09:19:06 +02:00
|
|
|
from flask import render_template
|
|
|
|
from flask import request
|
|
|
|
from flask import jsonify
|
|
|
|
from flask import session
|
|
|
|
from flask import abort
|
|
|
|
from flask import redirect
|
|
|
|
from flask import url_for
|
2016-10-11 15:23:40 +02:00
|
|
|
from flask_login import login_required, current_user
|
2016-08-19 09:19:06 +02:00
|
|
|
import werkzeug.exceptions as wz_exceptions
|
|
|
|
|
2017-05-24 15:43:34 +02:00
|
|
|
from pillar import current_app
|
2018-02-13 14:36:05 +01:00
|
|
|
from pillar.api.utils import utcnow
|
2016-08-19 09:19:06 +02:00
|
|
|
from pillar.web import system_util
|
2016-08-24 14:26:47 +02:00
|
|
|
from pillar.web import utils
|
2018-09-13 16:20:47 +02:00
|
|
|
from pillar.web.nodes import finders
|
2016-08-19 09:19:06 +02:00
|
|
|
from pillar.web.utils.jstree import jstree_get_children
|
2017-05-31 10:33:24 +02:00
|
|
|
import pillar.extension
|
|
|
|
|
2016-08-19 09:19:06 +02:00
|
|
|
from .forms import ProjectForm
|
|
|
|
from .forms import NodeTypeForm
|
|
|
|
|
|
|
|
blueprint = Blueprint('projects', __name__)
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
SYNC_GROUP_NODE_NAME = 'Blender Sync'
|
|
|
|
IMAGE_SHARING_GROUP_NODE_NAME = 'Image sharing'
|
|
|
|
|
|
|
|
|
2017-05-24 15:45:18 +02:00
|
|
|
def project_view(projections: dict = None):
|
|
|
|
"""Endpoint decorator, translates project_url into an actual project."""
|
|
|
|
|
|
|
|
import functools
|
|
|
|
import pillarsdk
|
|
|
|
|
|
|
|
if callable(projections):
|
|
|
|
raise TypeError('Use with @project_view() <-- note the parentheses')
|
|
|
|
|
|
|
|
def decorator(wrapped):
|
|
|
|
@functools.wraps(wrapped)
|
|
|
|
def wrapper(project_url, *args, **kwargs):
|
|
|
|
if isinstance(project_url, pillarsdk.Resource):
|
|
|
|
# This is already a resource, so this call probably is from one
|
|
|
|
# view to another. Assume the caller knows what he's doing and
|
|
|
|
# just pass everything along.
|
|
|
|
return wrapped(project_url, *args, **kwargs)
|
|
|
|
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
|
|
|
|
project = pillarsdk.Project.find_by_url(
|
|
|
|
project_url,
|
|
|
|
{'projection': projections} if projections else None,
|
|
|
|
api=api)
|
|
|
|
utils.attach_project_pictures(project, api)
|
|
|
|
|
|
|
|
return wrapped(project, *args, **kwargs)
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
2016-08-19 09:19:06 +02:00
|
|
|
@blueprint.route('/')
|
|
|
|
@login_required
|
|
|
|
def index():
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
|
|
|
|
# Get all projects, except the home project.
|
|
|
|
projects_user = Project.all({
|
|
|
|
'where': {'user': current_user.objectid,
|
|
|
|
'category': {'$ne': 'home'}},
|
|
|
|
'sort': '-_created'
|
|
|
|
}, api=api)
|
|
|
|
|
2017-12-22 16:27:05 +01:00
|
|
|
show_deleted_projects = request.args.get('deleted') is not None
|
|
|
|
if show_deleted_projects:
|
2018-02-13 14:36:05 +01:00
|
|
|
timeframe = utcnow() - datetime.timedelta(days=31)
|
2017-12-22 16:27:05 +01:00
|
|
|
projects_deleted = Project.all({
|
|
|
|
'where': {'user': current_user.objectid,
|
|
|
|
'category': {'$ne': 'home'},
|
|
|
|
'_deleted': True,
|
|
|
|
'_updated': {'$gt': timeframe}},
|
|
|
|
'sort': '-_created'
|
|
|
|
}, api=api)
|
|
|
|
else:
|
|
|
|
projects_deleted = {'_items': []}
|
|
|
|
|
2016-08-19 09:19:06 +02:00
|
|
|
projects_shared = Project.all({
|
|
|
|
'where': {'user': {'$ne': current_user.objectid},
|
|
|
|
'permissions.groups.group': {'$in': current_user.groups},
|
|
|
|
'is_private': True},
|
|
|
|
'sort': '-_created',
|
|
|
|
'embedded': {'user': 1},
|
|
|
|
}, api=api)
|
|
|
|
|
|
|
|
# Attach project images
|
2017-12-22 16:27:05 +01:00
|
|
|
for project_list in (projects_user, projects_deleted, projects_shared):
|
2018-01-30 15:52:55 +01:00
|
|
|
utils.mass_attach_project_pictures(project_list['_items'], api=api, header=False)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
return render_template(
|
|
|
|
'projects/index_dashboard.html',
|
2016-08-24 14:26:47 +02:00
|
|
|
gravatar=utils.gravatar(current_user.email, size=128),
|
2016-08-19 09:19:06 +02:00
|
|
|
projects_user=projects_user['_items'],
|
2017-12-22 16:27:05 +01:00
|
|
|
projects_deleted=projects_deleted['_items'],
|
2016-08-19 09:19:06 +02:00
|
|
|
projects_shared=projects_shared['_items'],
|
2017-12-22 16:27:05 +01:00
|
|
|
show_deleted_projects=show_deleted_projects,
|
2016-08-19 09:19:06 +02:00
|
|
|
api=api)
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/<project_url>/jstree')
|
2017-06-07 17:06:26 +02:00
|
|
|
def jstree(project_url):
|
2016-08-19 09:19:06 +02:00
|
|
|
"""Entry point to view a project as JSTree"""
|
2017-06-07 17:06:26 +02:00
|
|
|
api = system_util.pillar_api()
|
|
|
|
|
|
|
|
try:
|
|
|
|
project = Project.find_one({
|
|
|
|
'projection': {'_id': 1},
|
|
|
|
'where': {'url': project_url}
|
|
|
|
}, api=api)
|
|
|
|
except ResourceNotFound:
|
|
|
|
raise wz_exceptions.NotFound('No such project')
|
|
|
|
|
2016-08-19 09:19:06 +02:00
|
|
|
return jsonify(items=jstree_get_children(None, project._id))
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/home/')
|
|
|
|
@login_required
|
|
|
|
def home_project():
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
project = _home_project(api)
|
|
|
|
|
|
|
|
# Get the synchronised Blender versions
|
|
|
|
project_id = project['_id']
|
|
|
|
synced_versions = synced_blender_versions(project_id, api)
|
|
|
|
|
|
|
|
extra_context = {
|
|
|
|
'synced_versions': synced_versions,
|
|
|
|
'show_addon_download_buttons': True,
|
|
|
|
}
|
|
|
|
|
|
|
|
return render_project(project, api, extra_context)
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/home/images')
|
|
|
|
@login_required
|
|
|
|
def home_project_shared_images():
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
project = _home_project(api)
|
|
|
|
|
|
|
|
# Get the shared images
|
|
|
|
project_id = project['_id']
|
|
|
|
image_nodes = shared_image_nodes(project_id, api)
|
|
|
|
|
|
|
|
extra_context = {
|
|
|
|
'shared_images': image_nodes,
|
|
|
|
'show_addon_download_buttons': current_user.has_role('subscriber', 'demo'),
|
|
|
|
}
|
|
|
|
|
|
|
|
return render_project(project, api, extra_context,
|
|
|
|
template_name='projects/home_images.html')
|
|
|
|
|
|
|
|
|
|
|
|
def _home_project(api):
|
|
|
|
try:
|
|
|
|
project = Project.find_from_endpoint('/bcloud/home-project', api=api)
|
|
|
|
except ResourceNotFound:
|
|
|
|
log.warning('Home project for user %s not found', current_user.objectid)
|
|
|
|
raise wz_exceptions.NotFound('No such project')
|
|
|
|
|
|
|
|
return project
|
|
|
|
|
|
|
|
|
|
|
|
def synced_blender_versions(home_project_id, api):
|
|
|
|
"""Returns a list of Blender versions with synced settings.
|
|
|
|
|
|
|
|
Returns a list of {'version': '2.77', 'date': datetime.datetime()} dicts.
|
|
|
|
Returns an empty list if no Blender versions were synced.
|
|
|
|
"""
|
|
|
|
|
|
|
|
sync_group = Node.find_first({
|
|
|
|
'where': {'project': home_project_id,
|
|
|
|
'node_type': 'group',
|
|
|
|
'parent': None,
|
|
|
|
'name': SYNC_GROUP_NODE_NAME},
|
|
|
|
'projection': {'_id': 1}},
|
|
|
|
api=api)
|
|
|
|
|
|
|
|
if not sync_group:
|
|
|
|
return []
|
|
|
|
|
2018-04-09 13:41:08 +02:00
|
|
|
sync_nodes = Node.all(
|
|
|
|
{
|
|
|
|
'where': {'project': home_project_id,
|
|
|
|
'node_type': 'group',
|
|
|
|
'parent': sync_group['_id']},
|
|
|
|
'projection': {
|
|
|
|
'name': 1,
|
|
|
|
'_updated': 1,
|
|
|
|
},
|
|
|
|
'sort': [('_updated', -1)],
|
|
|
|
},
|
2016-08-19 09:19:06 +02:00
|
|
|
api=api)
|
|
|
|
|
|
|
|
sync_nodes = sync_nodes._items
|
|
|
|
if not sync_nodes:
|
|
|
|
return []
|
|
|
|
|
|
|
|
return [{'version': node.name, 'date': node._updated}
|
|
|
|
for node in sync_nodes]
|
|
|
|
|
|
|
|
|
|
|
|
def shared_image_nodes(home_project_id, api):
|
|
|
|
"""Returns a list of pillarsdk.Node objects."""
|
|
|
|
|
|
|
|
parent_group = Node.find_first({
|
|
|
|
'where': {'project': home_project_id,
|
|
|
|
'node_type': 'group',
|
|
|
|
'parent': None,
|
|
|
|
'name': IMAGE_SHARING_GROUP_NODE_NAME},
|
|
|
|
'projection': {'_id': 1}},
|
|
|
|
api=api)
|
|
|
|
|
|
|
|
if not parent_group:
|
|
|
|
log.debug('No image sharing parent node found.')
|
|
|
|
return []
|
|
|
|
|
|
|
|
nodes = Node.all({
|
|
|
|
'where': {'project': home_project_id,
|
|
|
|
'node_type': 'asset',
|
|
|
|
'properties.content_type': 'image',
|
|
|
|
'parent': parent_group['_id']},
|
|
|
|
'sort': '-_created',
|
|
|
|
'projection': {
|
|
|
|
'_created': 1,
|
|
|
|
'name': 1,
|
|
|
|
'picture': 1,
|
|
|
|
'short_code': 1,
|
|
|
|
}},
|
|
|
|
api=api)
|
|
|
|
|
|
|
|
nodes = nodes._items or []
|
|
|
|
for node in nodes:
|
2016-08-24 14:26:47 +02:00
|
|
|
node.picture = utils.get_file(node.picture)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
return nodes
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/home/jstree')
|
|
|
|
def home_jstree():
|
|
|
|
"""Entry point to view the home project as JSTree"""
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
|
|
|
|
try:
|
|
|
|
project = Project.find_from_endpoint('/bcloud/home-project',
|
|
|
|
params={'projection': {
|
|
|
|
'_id': 1,
|
|
|
|
'permissions': 1,
|
|
|
|
'category': 1,
|
|
|
|
'user': 1}},
|
|
|
|
api=api)
|
|
|
|
except ResourceNotFound:
|
|
|
|
raise wz_exceptions.NotFound('No such project')
|
|
|
|
|
|
|
|
return jsonify(items=jstree_get_children(None, project._id))
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/<project_url>/')
|
2017-06-07 17:06:26 +02:00
|
|
|
def view(project_url):
|
2016-08-19 09:19:06 +02:00
|
|
|
"""Entry point to view a project"""
|
|
|
|
|
|
|
|
if request.args.get('format') == 'jstree':
|
|
|
|
log.warning('projects.view(%r) endpoint called with format=jstree, '
|
|
|
|
'redirecting to proper endpoint. URL is %s; referrer is %s',
|
2017-06-07 17:06:26 +02:00
|
|
|
project_url, request.url, request.referrer)
|
|
|
|
return redirect(url_for('projects.jstree', project_url=project_url))
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
api = system_util.pillar_api()
|
2017-06-07 17:06:26 +02:00
|
|
|
project = find_project_or_404(project_url,
|
|
|
|
embedded={'header_node': 1},
|
|
|
|
api=api)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
# Load the header video file, if there is any.
|
|
|
|
header_video_file = None
|
|
|
|
header_video_node = None
|
|
|
|
if project.header_node and project.header_node.node_type == 'asset' and \
|
2017-12-22 16:27:16 +01:00
|
|
|
project.header_node.properties.content_type == 'video':
|
2017-05-24 15:48:21 +02:00
|
|
|
header_video_node = project.header_node
|
|
|
|
header_video_file = utils.get_file(project.header_node.properties.file)
|
|
|
|
header_video_node.picture = utils.get_file(header_video_node.picture)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
return render_project(project, api,
|
|
|
|
extra_context={'header_video_file': header_video_file,
|
|
|
|
'header_video_node': header_video_node})
|
|
|
|
|
|
|
|
|
2018-09-13 16:20:47 +02:00
|
|
|
def project_navigation_links(project, api) -> list:
|
|
|
|
"""Returns a list of nodes for the project, for top navigation display.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
project: A Project object.
|
|
|
|
api: the api client credential.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
A list of links for the Project.
|
|
|
|
For example we display a link to the project blog if present, as well
|
|
|
|
as pages. The list is structured as follows:
|
|
|
|
|
|
|
|
[{'url': '/p/spring/about', 'label': 'About'},
|
|
|
|
{'url': '/p/spring/blog', 'label': 'Blog'}]
|
|
|
|
"""
|
|
|
|
|
|
|
|
links = []
|
|
|
|
|
|
|
|
# Fetch the blog
|
|
|
|
blog = Node.find_first({
|
|
|
|
'where': {'project': project._id, 'node_type': 'blog', '_deleted': {'$ne': True}},
|
|
|
|
'projection': {
|
|
|
|
'name': 1,
|
|
|
|
}
|
|
|
|
}, api=api)
|
|
|
|
|
|
|
|
if blog:
|
|
|
|
links.append({'url': finders.find_url_for_node(blog), 'label': blog.name})
|
|
|
|
|
|
|
|
# Fetch pages
|
|
|
|
pages = Node.all({
|
|
|
|
'where': {'project': project._id, 'node_type': 'page', '_deleted': {'$ne': True}},
|
|
|
|
'projection': {
|
|
|
|
'name': 1,
|
|
|
|
'properties.url': 1
|
|
|
|
}
|
|
|
|
}, api=api)
|
|
|
|
|
|
|
|
# Process the results and append the links to the list
|
|
|
|
for p in pages._items:
|
|
|
|
|
|
|
|
links.append({'url': finders.find_url_for_node(p), 'label': p.name})
|
|
|
|
|
|
|
|
return links
|
|
|
|
|
|
|
|
|
2016-08-19 09:19:06 +02:00
|
|
|
def render_project(project, api, extra_context=None, template_name=None):
|
2016-08-24 14:26:47 +02:00
|
|
|
project.picture_square = utils.get_file(project.picture_square, api=api)
|
|
|
|
project.picture_header = utils.get_file(project.picture_header, api=api)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
2016-11-24 19:03:43 +01:00
|
|
|
def load_latest(list_of_ids, node_type=None):
|
2016-08-19 09:19:06 +02:00
|
|
|
"""Loads a list of IDs in reversed order."""
|
|
|
|
|
|
|
|
if not list_of_ids:
|
|
|
|
return []
|
|
|
|
|
|
|
|
# Construct query parameters outside the loop.
|
2016-09-30 17:42:58 +02:00
|
|
|
projection = {'name': 1, 'user': 1, 'node_type': 1, 'project': 1,
|
2016-11-24 18:14:07 +01:00
|
|
|
'properties.url': 1, 'properties.content_type': 1,
|
|
|
|
'picture': 1}
|
2016-08-19 09:19:06 +02:00
|
|
|
params = {'projection': projection, 'embedded': {'user': 1}}
|
|
|
|
|
2016-11-24 19:03:43 +01:00
|
|
|
if node_type == 'post':
|
|
|
|
projection['properties.content'] = 1
|
|
|
|
elif node_type == 'asset':
|
|
|
|
projection['description'] = 1
|
|
|
|
|
2016-08-19 09:19:06 +02:00
|
|
|
list_latest = []
|
|
|
|
for node_id in reversed(list_of_ids or ()):
|
|
|
|
try:
|
|
|
|
node_item = Node.find(node_id, params, api=api)
|
|
|
|
|
2016-08-24 14:26:47 +02:00
|
|
|
node_item.picture = utils.get_file(node_item.picture, api=api)
|
2016-08-19 09:19:06 +02:00
|
|
|
list_latest.append(node_item)
|
|
|
|
except ForbiddenAccess:
|
|
|
|
pass
|
|
|
|
except ResourceNotFound:
|
|
|
|
log.warning('Project %s refers to removed node %s!',
|
|
|
|
project._id, node_id)
|
|
|
|
|
|
|
|
return list_latest
|
|
|
|
|
2016-11-24 19:03:43 +01:00
|
|
|
project.nodes_featured = load_latest(project.nodes_featured, node_type='asset')
|
|
|
|
project.nodes_blog = load_latest(project.nodes_blog, node_type='post')
|
2016-11-24 18:13:46 +01:00
|
|
|
|
|
|
|
# Merge featured assets and blog posts into one activity stream
|
|
|
|
def sort_key(item):
|
|
|
|
return item._created
|
|
|
|
|
|
|
|
activities = itertools.chain(project.nodes_featured,
|
|
|
|
project.nodes_blog)
|
|
|
|
activity_stream = sorted(activities, key=sort_key, reverse=True)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
if extra_context is None:
|
|
|
|
extra_context = {}
|
|
|
|
|
|
|
|
if project.category == 'home' and not current_app.config['RENDER_HOME_AS_REGULAR_PROJECT']:
|
|
|
|
template_name = template_name or 'projects/home_index.html'
|
|
|
|
return render_template(
|
|
|
|
template_name,
|
2016-08-24 14:26:47 +02:00
|
|
|
gravatar=utils.gravatar(current_user.email, size=128),
|
2016-08-19 09:19:06 +02:00
|
|
|
project=project,
|
|
|
|
api=system_util.pillar_api(),
|
|
|
|
**extra_context)
|
|
|
|
|
|
|
|
if template_name is None:
|
|
|
|
if request.args.get('embed'):
|
|
|
|
embed_string = '_embed'
|
|
|
|
else:
|
|
|
|
embed_string = ''
|
|
|
|
template_name = "projects/view{0}.html".format(embed_string)
|
|
|
|
|
2016-10-11 16:33:44 +02:00
|
|
|
extension_sidebar_links = current_app.extension_sidebar_links(project)
|
|
|
|
|
2018-09-13 16:20:47 +02:00
|
|
|
navigation_links = project_navigation_links(project, api)
|
2018-09-10 16:11:21 +02:00
|
|
|
|
2016-08-19 09:19:06 +02:00
|
|
|
return render_template(template_name,
|
|
|
|
api=api,
|
|
|
|
project=project,
|
|
|
|
node=None,
|
|
|
|
show_node=False,
|
|
|
|
show_project=True,
|
|
|
|
og_picture=project.picture_header,
|
2016-11-24 18:13:46 +01:00
|
|
|
activity_stream=activity_stream,
|
2018-09-13 16:20:47 +02:00
|
|
|
navigation_links=navigation_links,
|
2016-10-11 16:33:44 +02:00
|
|
|
extension_sidebar_links=extension_sidebar_links,
|
2016-08-19 09:19:06 +02:00
|
|
|
**extra_context)
|
|
|
|
|
|
|
|
|
2016-12-02 16:57:48 +01:00
|
|
|
def render_node_page(project_url, page_url, api):
|
|
|
|
"""Custom behaviour for pages, which are nodes, but accessible on a custom
|
|
|
|
route base.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# TODO: ensure this is not called for the home project, as it would
|
|
|
|
# generate conflicting websites
|
|
|
|
project = find_project_or_404(project_url, api=api)
|
|
|
|
try:
|
|
|
|
page = Node.find_one({
|
|
|
|
'where': {
|
|
|
|
'project': project['_id'],
|
|
|
|
'node_type': 'page',
|
|
|
|
'properties.url': page_url}}, api=api)
|
|
|
|
except ResourceNotFound:
|
|
|
|
raise wz_exceptions.NotFound('No such node')
|
|
|
|
|
|
|
|
return project, page
|
|
|
|
|
|
|
|
|
2016-08-19 09:19:06 +02:00
|
|
|
@blueprint.route('/<project_url>/<node_id>')
|
|
|
|
def view_node(project_url, node_id):
|
|
|
|
"""Entry point to view a node in the context of a project"""
|
|
|
|
# Some browsers mangle URLs and URL-encode /p/{p-url}/#node-id
|
|
|
|
if node_id.startswith('#'):
|
|
|
|
return redirect(url_for('projects.view_node',
|
|
|
|
project_url=project_url,
|
|
|
|
node_id=node_id[1:]),
|
|
|
|
code=301) # permanent redirect
|
|
|
|
|
|
|
|
theatre_mode = 't' in request.args
|
2016-12-02 16:57:48 +01:00
|
|
|
api = system_util.pillar_api()
|
|
|
|
# First we check if it's a simple string, in which case we are looking for
|
|
|
|
# a static page. Maybe we could use bson.objectid.ObjectId.is_valid(node_id)
|
|
|
|
if not utils.is_valid_id(node_id):
|
|
|
|
# raise wz_exceptions.NotFound('No such node')
|
|
|
|
project, node = render_node_page(project_url, node_id, api)
|
|
|
|
else:
|
|
|
|
# Fetch the node before the project. If this user has access to the
|
|
|
|
# node, we should be able to get the project URL too.
|
|
|
|
try:
|
|
|
|
node = Node.find(node_id, api=api)
|
|
|
|
except ForbiddenAccess:
|
|
|
|
return render_template('errors/403.html'), 403
|
|
|
|
except ResourceNotFound:
|
|
|
|
raise wz_exceptions.NotFound('No such node')
|
|
|
|
|
|
|
|
try:
|
2017-05-24 15:48:21 +02:00
|
|
|
project = Project.find_one({'where': {"url": project_url, '_id': node.project}},
|
|
|
|
api=api)
|
2016-12-02 16:57:48 +01:00
|
|
|
except ResourceNotFound:
|
|
|
|
# In theatre mode, we don't need access to the project at all.
|
|
|
|
if theatre_mode:
|
|
|
|
project = None
|
|
|
|
else:
|
|
|
|
raise wz_exceptions.NotFound('No such project')
|
2016-08-19 09:19:06 +02:00
|
|
|
|
2016-08-24 14:26:47 +02:00
|
|
|
og_picture = node.picture = utils.get_file(node.picture, api=api)
|
2016-08-19 09:19:06 +02:00
|
|
|
if project:
|
|
|
|
if not node.picture:
|
2016-08-24 14:26:47 +02:00
|
|
|
og_picture = utils.get_file(project.picture_header, api=api)
|
|
|
|
project.picture_square = utils.get_file(project.picture_square, api=api)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
# Append _theatre to load the proper template
|
|
|
|
theatre = '_theatre' if theatre_mode else ''
|
2018-09-13 16:20:47 +02:00
|
|
|
navigation_links = project_navigation_links(project, api)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
2018-04-16 14:33:38 +02:00
|
|
|
if node.node_type == 'page':
|
|
|
|
return render_template('nodes/custom/page/view_embed.html',
|
|
|
|
api=api,
|
|
|
|
node=node,
|
|
|
|
project=project,
|
2018-09-13 16:20:47 +02:00
|
|
|
navigation_links=navigation_links,
|
2018-04-16 14:33:38 +02:00
|
|
|
og_picture=og_picture,)
|
|
|
|
|
2016-10-11 16:33:44 +02:00
|
|
|
extension_sidebar_links = current_app.extension_sidebar_links(project)
|
|
|
|
|
2016-08-19 09:19:06 +02:00
|
|
|
return render_template('projects/view{}.html'.format(theatre),
|
|
|
|
api=api,
|
|
|
|
project=project,
|
|
|
|
node=node,
|
|
|
|
show_node=True,
|
|
|
|
show_project=False,
|
2016-10-11 16:33:44 +02:00
|
|
|
og_picture=og_picture,
|
2018-09-13 16:20:47 +02:00
|
|
|
navigation_links=navigation_links,
|
2016-10-11 16:33:44 +02:00
|
|
|
extension_sidebar_links=extension_sidebar_links)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
|
|
|
|
def find_project_or_404(project_url, embedded=None, api=None):
|
|
|
|
"""Aborts with a NotFound exception when the project cannot be found."""
|
|
|
|
|
|
|
|
params = {'where': {"url": project_url}}
|
|
|
|
if embedded:
|
|
|
|
params['embedded'] = embedded
|
|
|
|
|
|
|
|
try:
|
|
|
|
project = Project.find_one(params, api=api)
|
|
|
|
except ResourceNotFound:
|
|
|
|
raise wz_exceptions.NotFound('No such project')
|
|
|
|
|
|
|
|
return project
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/<project_url>/search')
|
2017-06-07 17:06:26 +02:00
|
|
|
def search(project_url):
|
2016-08-19 09:19:06 +02:00
|
|
|
"""Search into a project"""
|
2017-06-07 17:06:26 +02:00
|
|
|
api = system_util.pillar_api()
|
|
|
|
project = find_project_or_404(project_url, api=api)
|
|
|
|
project.picture_square = utils.get_file(project.picture_square, api=api)
|
|
|
|
project.picture_header = utils.get_file(project.picture_header, api=api)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
return render_template('nodes/search.html',
|
|
|
|
project=project,
|
|
|
|
og_picture=project.picture_header)
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/<project_url>/edit', methods=['GET', 'POST'])
|
|
|
|
@login_required
|
2017-06-07 17:06:26 +02:00
|
|
|
def edit(project_url):
|
2016-08-19 09:19:06 +02:00
|
|
|
api = system_util.pillar_api()
|
2017-06-07 17:06:26 +02:00
|
|
|
# Fetch the Node or 404
|
|
|
|
try:
|
|
|
|
project = Project.find_one({'where': {'url': project_url}}, api=api)
|
|
|
|
# project = Project.find(project_url, api=api)
|
|
|
|
except ResourceNotFound:
|
|
|
|
abort(404)
|
|
|
|
utils.attach_project_pictures(project, api)
|
2016-08-19 09:19:06 +02:00
|
|
|
form = ProjectForm(
|
|
|
|
project_id=project._id,
|
|
|
|
name=project.name,
|
|
|
|
url=project.url,
|
|
|
|
summary=project.summary,
|
|
|
|
description=project.description,
|
2017-03-03 12:00:24 +01:00
|
|
|
is_private='GET' not in project.permissions.world,
|
2016-08-19 09:19:06 +02:00
|
|
|
category=project.category,
|
|
|
|
status=project.status,
|
|
|
|
)
|
|
|
|
|
|
|
|
if form.validate_on_submit():
|
|
|
|
project = Project.find(project._id, api=api)
|
|
|
|
project.name = form.name.data
|
|
|
|
project.url = form.url.data
|
|
|
|
project.summary = form.summary.data
|
|
|
|
project.description = form.description.data
|
|
|
|
project.category = form.category.data
|
|
|
|
project.status = form.status.data
|
|
|
|
if form.picture_square.data:
|
|
|
|
project.picture_square = form.picture_square.data
|
|
|
|
if form.picture_header.data:
|
|
|
|
project.picture_header = form.picture_header.data
|
|
|
|
|
|
|
|
# Update world permissions from is_private checkbox
|
|
|
|
if form.is_private.data:
|
|
|
|
project.permissions.world = []
|
|
|
|
else:
|
2017-03-03 12:00:24 +01:00
|
|
|
project.permissions.world = ['GET']
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
project.update(api=api)
|
|
|
|
# Reattach the pictures
|
2016-08-24 14:26:47 +02:00
|
|
|
utils.attach_project_pictures(project, api)
|
2016-08-19 09:19:06 +02:00
|
|
|
else:
|
|
|
|
if project.picture_square:
|
|
|
|
form.picture_square.data = project.picture_square._id
|
|
|
|
if project.picture_header:
|
|
|
|
form.picture_header.data = project.picture_header._id
|
|
|
|
|
|
|
|
# List of fields from the form that should be hidden to regular users
|
|
|
|
if current_user.has_role('admin'):
|
|
|
|
hidden_fields = []
|
|
|
|
else:
|
|
|
|
hidden_fields = ['url', 'status', 'is_private', 'category']
|
|
|
|
|
|
|
|
return render_template('projects/edit.html',
|
|
|
|
form=form,
|
|
|
|
hidden_fields=hidden_fields,
|
|
|
|
project=project,
|
2017-05-31 10:33:24 +02:00
|
|
|
ext_pages=find_extension_pages(),
|
2016-08-19 09:19:06 +02:00
|
|
|
api=api)
|
|
|
|
|
|
|
|
|
2017-05-31 10:33:24 +02:00
|
|
|
def find_extension_pages() -> typing.List[pillar.extension.PillarExtension]:
|
|
|
|
"""Returns a list of Pillar extensions that have project settings pages."""
|
|
|
|
|
|
|
|
return [ext for ext in current_app.pillar_extensions.values()
|
|
|
|
if ext.has_project_settings]
|
|
|
|
|
|
|
|
|
2016-08-19 09:19:06 +02:00
|
|
|
@blueprint.route('/<project_url>/edit/node-type')
|
|
|
|
@login_required
|
2017-06-07 17:06:26 +02:00
|
|
|
def edit_node_types(project_url):
|
2016-08-19 09:19:06 +02:00
|
|
|
api = system_util.pillar_api()
|
2017-06-07 17:06:26 +02:00
|
|
|
# Fetch the project or 404
|
|
|
|
try:
|
|
|
|
project = Project.find_one({
|
|
|
|
'where': '{"url" : "%s"}' % (project_url)}, api=api)
|
|
|
|
except ResourceNotFound:
|
|
|
|
return abort(404)
|
|
|
|
|
|
|
|
utils.attach_project_pictures(project, api)
|
|
|
|
|
2016-08-19 09:19:06 +02:00
|
|
|
return render_template('projects/edit_node_types.html',
|
|
|
|
api=api,
|
2017-05-31 10:33:24 +02:00
|
|
|
ext_pages=find_extension_pages(),
|
2016-08-19 09:19:06 +02:00
|
|
|
project=project)
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/<project_url>/e/node-type/<node_type_name>', methods=['GET', 'POST'])
|
|
|
|
@login_required
|
2017-06-07 17:06:26 +02:00
|
|
|
def edit_node_type(project_url, node_type_name):
|
2016-08-19 09:19:06 +02:00
|
|
|
api = system_util.pillar_api()
|
2017-06-07 17:06:26 +02:00
|
|
|
# Fetch the Node or 404
|
|
|
|
try:
|
2018-02-01 14:13:01 +01:00
|
|
|
project = Project.find_one({'where': {'url': project_url}}, api=api)
|
2017-06-07 17:06:26 +02:00
|
|
|
except ResourceNotFound:
|
|
|
|
return abort(404)
|
2018-02-01 14:13:01 +01:00
|
|
|
|
2017-06-07 17:06:26 +02:00
|
|
|
utils.attach_project_pictures(project, api)
|
2016-08-19 09:19:06 +02:00
|
|
|
node_type = project.get_node_type(node_type_name)
|
|
|
|
form = NodeTypeForm()
|
2018-02-01 14:13:01 +01:00
|
|
|
|
2016-08-19 09:19:06 +02:00
|
|
|
if form.validate_on_submit():
|
|
|
|
# Update dynamic & form schemas
|
|
|
|
dyn_schema = json.loads(form.dyn_schema.data)
|
|
|
|
node_type.dyn_schema = dyn_schema
|
|
|
|
form_schema = json.loads(form.form_schema.data)
|
|
|
|
node_type.form_schema = form_schema
|
2017-09-04 16:14:23 +02:00
|
|
|
node_type.description = form.description.data
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
# Update permissions
|
|
|
|
permissions = json.loads(form.permissions.data)
|
|
|
|
node_type.permissions = permissions
|
|
|
|
|
|
|
|
project.update(api=api)
|
|
|
|
elif request.method == 'GET':
|
|
|
|
form.project_id.data = project._id
|
|
|
|
if node_type:
|
|
|
|
form.name.data = node_type.name
|
|
|
|
form.description.data = node_type.description
|
|
|
|
form.parent.data = node_type.parent
|
|
|
|
|
|
|
|
dyn_schema = node_type.dyn_schema.to_dict()
|
|
|
|
form_schema = node_type.form_schema.to_dict()
|
|
|
|
if 'permissions' in node_type:
|
|
|
|
permissions = node_type.permissions.to_dict()
|
|
|
|
else:
|
|
|
|
permissions = {}
|
|
|
|
|
|
|
|
form.form_schema.data = json.dumps(form_schema, indent=4)
|
|
|
|
form.dyn_schema.data = json.dumps(dyn_schema, indent=4)
|
|
|
|
form.permissions.data = json.dumps(permissions, indent=4)
|
2017-09-04 16:17:08 +02:00
|
|
|
|
2018-02-01 14:13:01 +01:00
|
|
|
if request.method == 'POST':
|
|
|
|
# Send back a JSON response, as this is actually called
|
|
|
|
# from JS instead of rendered as page.
|
|
|
|
if form.errors:
|
|
|
|
resp = jsonify({'_message': str(form.errors)})
|
|
|
|
resp.status_code = 400
|
|
|
|
return resp
|
|
|
|
return jsonify({'_message': 'ok'})
|
|
|
|
|
2017-09-04 16:17:08 +02:00
|
|
|
return render_template('projects/edit_node_type_embed.html',
|
2016-08-19 09:19:06 +02:00
|
|
|
form=form,
|
|
|
|
project=project,
|
|
|
|
api=api,
|
|
|
|
node_type=node_type)
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/<project_url>/edit/sharing', methods=['GET', 'POST'])
|
|
|
|
@login_required
|
2017-06-07 17:06:26 +02:00
|
|
|
def sharing(project_url):
|
2016-08-19 09:19:06 +02:00
|
|
|
api = system_util.pillar_api()
|
2017-06-07 17:06:26 +02:00
|
|
|
# Fetch the project or 404
|
|
|
|
try:
|
|
|
|
project = Project.find_one({
|
|
|
|
'where': '{"url" : "%s"}' % (project_url)}, api=api)
|
|
|
|
except ResourceNotFound:
|
|
|
|
return abort(404)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
# Fetch users that are part of the admin group
|
|
|
|
users = project.get_users(api=api)
|
|
|
|
for user in users['_items']:
|
2016-08-24 14:26:47 +02:00
|
|
|
user['avatar'] = utils.gravatar(user['email'])
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
if request.method == 'POST':
|
|
|
|
user_id = request.form['user_id']
|
|
|
|
action = request.form['action']
|
|
|
|
try:
|
|
|
|
if action == 'add':
|
|
|
|
user = project.add_user(user_id, api=api)
|
|
|
|
elif action == 'remove':
|
|
|
|
user = project.remove_user(user_id, api=api)
|
|
|
|
except ResourceNotFound:
|
2017-06-07 17:06:26 +02:00
|
|
|
log.info('/p/%s/edit/sharing: User %s not found', project_url, user_id)
|
2016-08-19 09:19:06 +02:00
|
|
|
return jsonify({'_status': 'ERROR',
|
|
|
|
'message': 'User %s not found' % user_id}), 404
|
|
|
|
|
|
|
|
# Add gravatar to user
|
2016-08-24 14:26:47 +02:00
|
|
|
user['avatar'] = utils.gravatar(user['email'])
|
2016-08-19 09:19:06 +02:00
|
|
|
return jsonify(user)
|
|
|
|
|
2017-06-07 17:06:26 +02:00
|
|
|
utils.attach_project_pictures(project, api)
|
|
|
|
|
2016-08-19 09:19:06 +02:00
|
|
|
return render_template('projects/sharing.html',
|
|
|
|
api=api,
|
|
|
|
project=project,
|
2017-05-31 10:33:24 +02:00
|
|
|
ext_pages=find_extension_pages(),
|
2016-08-19 09:19:06 +02:00
|
|
|
users=users['_items'])
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/e/add-featured-node', methods=['POST'])
|
|
|
|
@login_required
|
|
|
|
def add_featured_node():
|
|
|
|
"""Feature a node in a project. This method belongs here, because it affects
|
|
|
|
the project node itself, not the asset.
|
|
|
|
"""
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
node = Node.find(request.form['node_id'], api=api)
|
|
|
|
action = project_update_nodes_list(node, project_id=node.project, list_name='featured')
|
|
|
|
return jsonify(status='success', data=dict(action=action))
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/e/move-node', methods=['POST'])
|
|
|
|
@login_required
|
|
|
|
def move_node():
|
|
|
|
"""Move a node within a project. While this affects the node.parent prop, we
|
|
|
|
keep it in the scope of the project."""
|
|
|
|
node_id = request.form['node_id']
|
|
|
|
dest_parent_node_id = request.form.get('dest_parent_node_id')
|
|
|
|
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
node = Node.find(node_id, api=api)
|
|
|
|
# Get original parent id for clearing template fragment on success
|
|
|
|
previous_parent_id = node.parent
|
|
|
|
if dest_parent_node_id:
|
|
|
|
node.parent = dest_parent_node_id
|
|
|
|
elif node.parent:
|
|
|
|
node.parent = None
|
|
|
|
node.update(api=api)
|
|
|
|
return jsonify(status='success', data=dict(message='node moved'))
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/e/delete-node', methods=['POST'])
|
|
|
|
@login_required
|
|
|
|
def delete_node():
|
|
|
|
"""Delete a node"""
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
node = Node.find(request.form['node_id'], api=api)
|
|
|
|
if not node.has_method('DELETE'):
|
|
|
|
return abort(403)
|
|
|
|
|
|
|
|
node.delete(api=api)
|
|
|
|
|
|
|
|
return jsonify(status='success', data=dict(message='Node deleted'))
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/e/toggle-node-public', methods=['POST'])
|
|
|
|
@login_required
|
|
|
|
def toggle_node_public():
|
|
|
|
"""Give a node GET permissions for the world. Later on this can turn into
|
|
|
|
a more powerful permission management function.
|
|
|
|
"""
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
node = Node.find(request.form['node_id'], api=api)
|
|
|
|
if node.has_method('PUT'):
|
|
|
|
if node.permissions and 'world' in node.permissions.to_dict():
|
|
|
|
node.permissions = {}
|
|
|
|
message = "Node is not public anymore."
|
|
|
|
else:
|
|
|
|
node.permissions = dict(world=['GET'])
|
|
|
|
message = "Node is now public!"
|
|
|
|
node.update(api=api)
|
|
|
|
return jsonify(status='success', data=dict(message=message))
|
|
|
|
else:
|
|
|
|
return abort(403)
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/e/toggle-node-project-header', methods=['POST'])
|
|
|
|
@login_required
|
|
|
|
def toggle_node_project_header():
|
|
|
|
"""Sets this node as the project header, or removes it if already there.
|
|
|
|
"""
|
|
|
|
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
node_id = request.form['node_id']
|
|
|
|
|
|
|
|
try:
|
|
|
|
node = Node.find(node_id, {'projection': {'project': 1}}, api=api)
|
|
|
|
except ResourceNotFound:
|
|
|
|
log.info('User %s trying to toggle non-existing node %s as project header',
|
|
|
|
current_user.objectid, node_id)
|
|
|
|
return jsonify(_status='ERROR', message='Node not found'), 404
|
|
|
|
|
|
|
|
try:
|
|
|
|
project = Project.find(node.project, api=api)
|
|
|
|
except ResourceNotFound:
|
|
|
|
log.info('User %s trying to toggle node %s as project header, but project %s not found',
|
|
|
|
current_user.objectid, node_id, node.project)
|
|
|
|
return jsonify(_status='ERROR', message='Project not found'), 404
|
|
|
|
|
|
|
|
# Toggle header node
|
|
|
|
if project.header_node == node_id:
|
|
|
|
log.debug('Un-setting header node of project %s', node.project)
|
|
|
|
project.header_node = None
|
|
|
|
action = 'unset'
|
|
|
|
else:
|
|
|
|
log.debug('Setting node %s as header of project %s', node_id, node.project)
|
|
|
|
project.header_node = node_id
|
|
|
|
action = 'set'
|
|
|
|
|
|
|
|
# Save the project
|
|
|
|
project.update(api=api)
|
|
|
|
|
|
|
|
return jsonify({'_status': 'OK',
|
|
|
|
'action': action})
|
|
|
|
|
|
|
|
|
|
|
|
def project_update_nodes_list(node, project_id=None, list_name='latest'):
|
|
|
|
"""Update the project node with the latest edited or favorited node.
|
|
|
|
The list value can be 'latest' or 'featured' and it will determined where
|
|
|
|
the node reference will be placed in.
|
|
|
|
"""
|
|
|
|
if node.properties.status and node.properties.status == 'published':
|
|
|
|
if not project_id and 'current_project_id' in session:
|
|
|
|
project_id = session['current_project_id']
|
|
|
|
elif not project_id:
|
|
|
|
return None
|
|
|
|
project_id = node.project
|
2017-03-03 12:00:24 +01:00
|
|
|
if type(project_id) is not str:
|
2016-08-19 09:19:06 +02:00
|
|
|
project_id = node.project._id
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
project = Project.find(project_id, api=api)
|
|
|
|
if list_name == 'latest':
|
|
|
|
nodes_list = project.nodes_latest
|
|
|
|
elif list_name == 'blog':
|
|
|
|
nodes_list = project.nodes_blog
|
|
|
|
else:
|
|
|
|
nodes_list = project.nodes_featured
|
|
|
|
|
|
|
|
if not nodes_list:
|
|
|
|
node_list_name = 'nodes_' + list_name
|
|
|
|
project[node_list_name] = []
|
|
|
|
nodes_list = project[node_list_name]
|
2016-11-25 13:32:05 +01:00
|
|
|
elif len(nodes_list) > 15:
|
2016-08-19 09:19:06 +02:00
|
|
|
nodes_list.pop(0)
|
|
|
|
|
|
|
|
if node._id in nodes_list:
|
|
|
|
# Pop to put this back on top of the list
|
|
|
|
nodes_list.remove(node._id)
|
|
|
|
if list_name == 'featured':
|
|
|
|
# We treat the action as a toggle and do not att the item back
|
|
|
|
project.update(api=api)
|
|
|
|
return "removed"
|
|
|
|
|
|
|
|
nodes_list.append(node._id)
|
|
|
|
project.update(api=api)
|
|
|
|
return "added"
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/create')
|
|
|
|
@login_required
|
|
|
|
def create():
|
|
|
|
"""Create a new project. This is a multi step operation that involves:
|
|
|
|
- initialize basic node types
|
|
|
|
- initialize basic permissions
|
|
|
|
- create and connect storage space
|
|
|
|
"""
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
project_properties = dict(
|
|
|
|
name='My project',
|
|
|
|
user=current_user.objectid,
|
|
|
|
category='assets',
|
|
|
|
status='pending'
|
|
|
|
)
|
|
|
|
project = Project(project_properties)
|
|
|
|
project.create(api=api)
|
|
|
|
|
|
|
|
return redirect(url_for('projects.edit',
|
|
|
|
project_url="p-{}".format(project['_id'])))
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/delete', methods=['POST'])
|
|
|
|
@login_required
|
|
|
|
def delete():
|
|
|
|
"""Unapologetically deletes a project"""
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
project_id = request.form['project_id']
|
|
|
|
project = Project.find(project_id, api=api)
|
|
|
|
project.delete(api=api)
|
|
|
|
return jsonify(dict(staus='success', data=dict(
|
|
|
|
message='Project deleted {}'.format(project['_id']))))
|
2017-05-24 15:48:06 +02:00
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/<project_url>/edit/<extension_name>', methods=['GET', 'POST'])
|
|
|
|
@login_required
|
|
|
|
@project_view()
|
|
|
|
def edit_extension(project: Project, extension_name):
|
|
|
|
try:
|
|
|
|
ext = current_app.pillar_extensions[extension_name]
|
|
|
|
except KeyError:
|
|
|
|
raise wz_exceptions.NotFound()
|
|
|
|
|
2017-05-31 10:33:24 +02:00
|
|
|
return ext.project_settings(project,
|
|
|
|
ext_pages=find_extension_pages())
|