Files
attract/attract/tasks/routes.py
Tobias Johansson bae39ce01d Attract multi edit: Edit multiple tasks/shots/assets at the same time
For the user:
Ctrl + L-Mouse to select multiple tasks/shots/assets and then edit
the nodes as before. When multiple items are selected a chain icon
can be seen in editor next to the fields. If the chain is broken
it indicates that the values are not the same on all the selected
items.

When a field has been edited it will be marked with a green background
color.

The items are saved one by one in parallel. This means that one item
could fail to be saved, while the others get updated.

For developers:
The editor and activities has been ported to Vue. The table and has
been updated to support multi select.

MultiEditEngine is the core of the multi edit. It keeps track of
what values differs and what has been edited.
2019-03-13 13:53:40 +01:00

200 lines
7.6 KiB
Python

import logging
from dateutil import parser
from flask import Blueprint, render_template, request, current_app, session
import flask
import flask_login
import werkzeug.exceptions as wz_exceptions
import pillarsdk
from pillar.web.projects.routes import project_navigation_links
from pillar.web.system_util import pillar_api
import pillar.api.utils
import pillar.web.subquery
from pillar.auth import current_user
from attract.routes import attract_project_view
from attract.node_types.task import node_type_task
from attract.node_types.shot import node_type_shot
from attract import current_attract, EXTENSION_NAME
blueprint = Blueprint('attract.tasks', __name__, url_prefix='/tasks')
perproject_blueprint = Blueprint('attract.tasks.perproject', __name__,
url_prefix='/<project_url>/tasks')
log = logging.getLogger(__name__)
@blueprint.route('/')
def index():
user = flask_login.current_user
if not user.is_authenticated:
return render_template('attract/tasks/index.html')
project = session.get('attract_last_project')
tasks = current_attract.task_manager.tasks_for_user(user.objectid)
return render_template('attract/tasks/for_user.html',
tasks=tasks['_items'],
project=project,
task_count=tasks['_meta']['total'])
@blueprint.route('/<task_id>', methods=['DELETE'])
@flask_login.login_required
def delete(task_id):
log.info('Deleting task %s', task_id)
etag = request.form['etag']
current_attract.task_manager.delete_task(task_id, etag)
return '', 204
@perproject_blueprint.route('/', endpoint='index')
@attract_project_view(extension_props=False)
def for_project(project, task_id=None):
tasks = current_attract.task_manager.tasks_for_project(project['_id'])
can_use_attract = current_attract.auth.current_user_may(current_attract.auth.Actions.USE)
navigation_links = project_navigation_links(project, pillar_api())
extension_sidebar_links = current_app.extension_sidebar_links(project)
return render_template('attract/tasks/for_project.html',
tasks=tasks['_items'],
selected_id=task_id,
project=project,
can_use_attract=can_use_attract,
can_create_task=can_use_attract,
navigation_links=navigation_links,
extension_sidebar_links=extension_sidebar_links)
@perproject_blueprint.route('/<task_id>')
@attract_project_view(extension_props=True)
def view_task(project, attract_props, task_id):
if not request.is_xhr:
return for_project(project, task_id=task_id)
# Task list is public, task details are not.
if not current_user.has_cap('attract-view'):
raise wz_exceptions.Forbidden()
api = pillar_api()
task = pillarsdk.Node.find(task_id, api=api)
node_type = project.get_node_type(node_type_task['name'])
# Figure out which task types are available, defaulting to the shot task types.
context = request.args.get('context', None) or 'shot'
task_types = task_types_given_context(project, attract_props, context, task)
if task.properties.due_date:
task.properties.due_date = parser.parse('%s' % task.properties.due_date)
# Fetch project users so that we can assign them tasks
auth = current_attract.auth
can_use_attract = auth.current_user_may(auth.Actions.USE)
can_edit = 'PUT' in task.allowed_methods and can_use_attract
if can_edit:
users = project.get_users(api=api)
project.users = users['_items']
else:
try:
user_ids = task.properties.assigned_to.users
except AttributeError:
task.properties['assigned_to'] = {'users': []}
else:
task.properties.assigned_to.users = [pillar.web.subquery.get_user_info(uid)
for uid in user_ids]
return render_template('attract/tasks/view_task_embed.html',
task=task,
project=project,
task_node_type=node_type,
task_types=task_types,
attract_props=attract_props.to_dict(),
attract_context=request.args.get('context'),
can_use_attract=can_use_attract,
can_edit=can_edit)
def task_types_given_context(project, attract_props, page_context, task):
"""Returns a list of task types, given the page context and/or task parent type."""
# If we're in an explicit shot/asset context, just use that.
if page_context in {'shot', 'asset'}:
ctx_node_type_name = '%s_%s' % (EXTENSION_NAME, page_context)
try:
return attract_props['task_types'][ctx_node_type_name]
except KeyError:
log.warning('Project %s does not have an Attract task type definition for %s',
project['_id'], ctx_node_type_name)
# Fall through to the case below.
# If we're not in such a context, we need to inspect the parent node type (if any).
if task.parent:
api = pillar_api()
parent = pillarsdk.Node.find(task.parent, {'projection': {'node_type': 1}}, api=api)
if parent:
try:
return attract_props['task_types'][parent['node_type']]
except KeyError:
log.warning('Project %s does not have an Attract task type definition for %s',
project['_id'], parent['node_type'])
# Fall through to the fallback case below.
# Just fall back to shot task types
try:
return attract_props['task_types'][node_type_shot['name']]
except KeyError:
log.warning('Project %s does not have an Attract task type definition for %s',
project['_id'], parent['node_type'])
# Fall through to the fallback case below.
# Fallback in case of total failure.
return []
@perproject_blueprint.route('/<task_id>', methods=['POST'])
@attract_project_view()
def save(project, task_id):
log.info('Saving task %s', task_id)
log.debug('Form data: %s', request.form)
task_dict = request.form.to_dict()
task_dict['users'] = request.form.getlist('users')
task = current_attract.task_manager.edit_task(task_id, **task_dict)
return pillar.api.utils.jsonify(task.to_dict())
@perproject_blueprint.route('/create', methods=['POST'])
@attract_project_view()
def create_task(project):
task_type = request.form['task_type']
parent = request.form.get('parent', None)
task = current_attract.task_manager.create_task(project,
task_type=task_type,
parent=parent)
resp = flask.make_response()
resp.headers['Location'] = flask.url_for('.view_task',
project_url=project['url'],
task_id=task['_id'])
resp.status_code = 201
return flask.make_response(flask.jsonify(task.to_dict()), 201)
@perproject_blueprint.route('/<task_id>/activities')
@attract_project_view()
def activities(project, task_id):
if not request.is_xhr:
return flask.redirect(flask.url_for('.view_task',
project_url=project.url,
task_id=task_id))
acts = current_attract.activities_for_node(task_id)
return flask.render_template('attract/tasks/view_activities_embed.html',
activities=acts)