* Removed main drop down menu * Added "My cloud" to user menu * Attract/Flamenco is found under Production Tools menu * Attract/Flamenco has the same navigation as its project
262 lines
9.4 KiB
Python
262 lines
9.4 KiB
Python
import functools
|
|
import logging
|
|
|
|
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, session
|
|
import flask_login
|
|
import werkzeug.exceptions as wz_exceptions
|
|
|
|
from pillar.auth import current_user as current_user
|
|
from pillar.api.utils import str2id
|
|
from pillar.web.utils import mass_attach_project_pictures
|
|
import pillar.web.subquery
|
|
from pillar.web.system_util import pillar_api
|
|
from pillar.web.projects.routes import project_view, project_navigation_links
|
|
import pillarsdk
|
|
|
|
from attract import current_attract
|
|
from attract.node_types.task import node_type_task
|
|
from attract.node_types.shot import node_type_shot
|
|
from attract.node_types.asset import node_type_asset
|
|
|
|
blueprint = Blueprint('attract', __name__)
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
@blueprint.route('/')
|
|
def index():
|
|
api = pillar_api()
|
|
|
|
user = flask_login.current_user
|
|
if user.is_authenticated:
|
|
tasks = current_attract.task_manager.tasks_for_user(user.objectid)
|
|
|
|
else:
|
|
tasks = None
|
|
|
|
# TODO: add projections.
|
|
projects = current_attract.attract_projects()
|
|
|
|
if current_user.is_anonymous:
|
|
# Headers are only shown in index_anon_left_column.pug
|
|
mass_attach_project_pictures(projects['_items'], square=False, api=api)
|
|
|
|
projs_with_summaries = [
|
|
(proj, current_attract.shot_manager.shot_status_summary(proj['_id']))
|
|
for proj in projects['_items']
|
|
]
|
|
|
|
# Fetch all activities for all Attract projects.
|
|
id_to_proj = {p['_id']: p for p in projects['_items']}
|
|
activities = pillarsdk.Activity.all({
|
|
'where': {
|
|
'project': {'$in': list(id_to_proj.keys())},
|
|
},
|
|
'sort': [('_created', -1)],
|
|
'max_results': 20,
|
|
}, api=api)
|
|
|
|
# Fetch more info for each activity.
|
|
for act in activities['_items']:
|
|
act.actor_user = pillar.web.subquery.get_user_info(act.actor_user)
|
|
act.project = id_to_proj[act.project]
|
|
try:
|
|
act.link = current_attract.link_for_activity(act)
|
|
except (ValueError, wz_exceptions.NotFound):
|
|
act.link = None
|
|
|
|
project = session.get('attract_last_project')
|
|
return render_template('attract/index.html',
|
|
tasks=tasks,
|
|
projs_with_summaries=projs_with_summaries,
|
|
activities=activities,
|
|
project=project)
|
|
|
|
|
|
def error_project_not_setup_for_attract():
|
|
return render_template('attract/errors/project_not_setup.html')
|
|
|
|
|
|
def attract_project_view(extra_project_projections: dict=None, extension_props=False, *,
|
|
full_project=False):
|
|
"""Decorator, replaces the first parameter project_url with the actual project.
|
|
|
|
Assumes the first parameter to the decorated function is 'project_url'. It then
|
|
looks up that project, checks that it's set up for Attract, and passes it to the
|
|
decorated function.
|
|
|
|
If not set up for attract, uses error_project_not_setup_for_attract() to render
|
|
the response.
|
|
|
|
:param extra_project_projections: extra projections to use on top of the ones already
|
|
used by this decorator.
|
|
:param extension_props: whether extension properties should be included. Includes them
|
|
in the projections, and verifies that they are there.
|
|
:param full_project: skip projections altogether, fetching the whole project.
|
|
"""
|
|
|
|
if callable(extra_project_projections):
|
|
raise TypeError('Use with @attract_project_view() <-- note the parentheses')
|
|
|
|
projections = {
|
|
'_id': 1,
|
|
'name': 1,
|
|
'node_types': 1,
|
|
'nodes_featured': 1,
|
|
'extension_props': 1,
|
|
# We don't need this here, but this way the wrapped function has access
|
|
# to the orignal URL passed to it.
|
|
'url': 1,
|
|
}
|
|
if extra_project_projections:
|
|
projections.update(extra_project_projections)
|
|
|
|
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)
|
|
|
|
if current_user.is_anonymous:
|
|
log.debug('attract_project_view: Anonymous user never has access to Attract.')
|
|
raise wz_exceptions.Forbidden()
|
|
|
|
api = pillar_api()
|
|
|
|
projection_param = None if full_project else {'projection': projections}
|
|
project = pillarsdk.Project.find_by_url(
|
|
project_url,
|
|
projection_param,
|
|
api=api)
|
|
|
|
is_attract = current_attract.is_attract_project(project,
|
|
test_extension_props=extension_props)
|
|
if not is_attract:
|
|
return error_project_not_setup_for_attract()
|
|
|
|
session['attract_last_project'] = project.to_dict()
|
|
|
|
# Check user access.
|
|
auth = current_attract.auth
|
|
auth.determine_user_rights(str2id(project['_id']))
|
|
if not auth.current_user_may(auth.Actions.VIEW):
|
|
log.info('User %s not allowed to use Attract', current_user)
|
|
raise wz_exceptions.Forbidden()
|
|
|
|
if extension_props:
|
|
pprops = project.extension_props.attract
|
|
return wrapped(project, pprops, *args, **kwargs)
|
|
return wrapped(project, *args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
|
|
@blueprint.route('/<project_url>')
|
|
@attract_project_view(extension_props=False)
|
|
def project_index(project):
|
|
return redirect(url_for('attract.shots.perproject.index', project_url=project.url))
|
|
|
|
|
|
@blueprint.route('/<project_url>/help')
|
|
@attract_project_view(extension_props=False)
|
|
def help(project):
|
|
nt_task = project.get_node_type(node_type_task['name'])
|
|
nt_shot = project.get_node_type(node_type_shot['name'])
|
|
|
|
statuses = set(nt_task['dyn_schema']['status']['allowed'] +
|
|
nt_shot['dyn_schema']['status']['allowed'])
|
|
|
|
return render_template('attract/help.html', statuses=statuses)
|
|
|
|
|
|
def project_settings(project: pillarsdk.Project, **template_args: dict):
|
|
"""Renders the project settings page for Attract projects."""
|
|
|
|
from . import EXTENSION_NAME
|
|
|
|
if not current_attract.auth.current_user_is_attract_user():
|
|
raise wz_exceptions.Forbidden()
|
|
|
|
# Based on the project state, we can render a different template.
|
|
if not current_attract.is_attract_project(project):
|
|
return render_template('attract/project_settings/offer_setup.html',
|
|
project=project, **template_args)
|
|
|
|
ntn_shot = node_type_shot['name']
|
|
ntn_asset = node_type_asset['name']
|
|
|
|
try:
|
|
attract_props = project['extension_props'][EXTENSION_NAME]
|
|
except KeyError:
|
|
# Not set up for attract, can happen.
|
|
shot_task_types = []
|
|
asset_task_types = []
|
|
else:
|
|
shot_task_types = attract_props['task_types'][ntn_shot]
|
|
asset_task_types = attract_props['task_types'][ntn_asset]
|
|
|
|
return render_template('attract/project_settings/settings.html',
|
|
project=project,
|
|
asset_node_type_name=ntn_asset,
|
|
shot_node_type_name=ntn_shot,
|
|
shot_task_types=shot_task_types,
|
|
asset_task_types=asset_task_types,
|
|
**template_args)
|
|
|
|
|
|
@blueprint.route('/<project_url>/<node_type_name>/set-task-types', methods=['POST'])
|
|
@attract_project_view(extension_props=True, full_project=True)
|
|
def save_task_types(project, attract_props, node_type_name: str):
|
|
from . import EXTENSION_NAME
|
|
from . import setup
|
|
import re
|
|
from collections import OrderedDict
|
|
|
|
valid_name_re = re.compile(r'^[0-9a-zA-Z &+\-.,_]+$')
|
|
|
|
if (not node_type_name.startswith('%s_' % EXTENSION_NAME)
|
|
or node_type_name not in attract_props['task_types']
|
|
or not valid_name_re.match(node_type_name)):
|
|
log.info('%s: received invalid node type name %r', request.endpoint, node_type_name)
|
|
raise wz_exceptions.BadRequest('Invalid node type name')
|
|
|
|
task_types_field = request.form.get('task_types')
|
|
if not task_types_field:
|
|
raise wz_exceptions.BadRequest('No task types given')
|
|
|
|
task_types = [
|
|
tt for tt in (tt.strip()
|
|
for tt in task_types_field.split('\n'))
|
|
if tt
|
|
]
|
|
task_types = list(OrderedDict.fromkeys(task_types)) # removes duplicates, maintains order.
|
|
if not all(valid_name_re.match(tt) for tt in task_types):
|
|
raise wz_exceptions.BadRequest('Invalid task type given')
|
|
|
|
setup.set_task_types(project.to_dict(), node_type_name, task_types)
|
|
|
|
return jsonify(task_types=task_types)
|
|
|
|
|
|
@blueprint.route('/<project_url>/setup-for-attract', methods=['POST'])
|
|
@flask_login.login_required
|
|
@project_view()
|
|
def setup_for_attract(project: pillarsdk.Project):
|
|
import attract.setup
|
|
|
|
project_id = project._id
|
|
|
|
if not project.has_method('PUT'):
|
|
log.warning('User %s tries to set up project %s for Attract, but has no PUT rights.',
|
|
current_user, project_id)
|
|
raise wz_exceptions.Forbidden()
|
|
|
|
log.info('User %s sets up project %s for Attract', current_user, project_id)
|
|
attract.setup.setup_for_attract(project.url)
|
|
|
|
return '', 204
|