Added project settings allowing setup + editing task types.

- Attract added to Project Settings screen
- setting up project for Attract
- editing shot/asset task types

To do: add checks that the user is allowed to use Attract in the first
place.
This commit is contained in:
2017-06-14 17:35:14 +02:00
parent 725f93175c
commit 9ea75c30e3
7 changed files with 252 additions and 14 deletions

View File

@@ -151,10 +151,13 @@ class AttractExtension(PillarExtension):
import pprint
self._log.debug('Project: %s', pprint.pformat(project.to_dict()))
return False
except KeyError:
# Not set up for Attract
return False
if pprops is None:
self._log.warning("is_attract_project: Project url=%r doesn't have Attract"
" extension properties.", project['url'])
self._log.debug("is_attract_project: Project url=%r doesn't have Attract"
" extension properties.", project['url'])
return False
return True
@@ -166,6 +169,20 @@ class AttractExtension(PillarExtension):
return flask.render_template('attract/sidebar.html',
project=project)
def project_settings(self, project: pillarsdk.Project, **template_args: dict) -> flask.Response:
"""Renders the project settings page for this extension.
Set YourExtension.has_project_settings = True and Pillar will call this function.
:param project: the project for which to render the settings.
:param template_args: additional template arguments.
:returns: a Flask HTTP response
"""
from attract.routes import project_settings
return project_settings(project, **template_args)
def activities_for_node(self, node_id, max_results=20, page=1):
"""Returns a page of activities for the given task or shot.
@@ -221,11 +238,11 @@ class AttractExtension(PillarExtension):
return url_for_node(node_id=act.object)
def _get_current_attract():
def _get_current_attract() -> AttractExtension:
"""Returns the Attract extension of the current application."""
return flask.current_app.pillar_extensions[EXTENSION_NAME]
current_attract = LocalProxy(_get_current_attract)
current_attract: AttractExtension = LocalProxy(_get_current_attract)
"""Attract extension of the current app."""

View File

@@ -1,18 +1,21 @@
import functools
import logging
from flask import Blueprint, render_template, redirect, url_for
from flask import Blueprint, render_template, redirect, url_for, request, jsonify
import flask_login
import werkzeug.exceptions as wz_exceptions
from pillar.auth import current_web_user as current_user
from pillar.web.utils import attach_project_pictures
import pillar.web.subquery
from pillar.web.system_util import pillar_api
from pillar.web.projects.routes import project_view
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__)
@@ -38,7 +41,7 @@ def index():
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']}
@@ -137,8 +140,8 @@ def attract_project_view(extra_project_projections=None, extension_props=False):
@blueprint.route('/<project_url>')
@attract_project_view(extension_props=True)
def project_index(project, attract_props):
@attract_project_view(extension_props=False)
def project_index(project):
return redirect(url_for('attract.shots.perproject.index', project_url=project.url))
@@ -152,3 +155,88 @@ def help(project):
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
# 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)
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

View File

@@ -1,11 +1,7 @@
"""Setting up projects for Attract.
"""Setting up projects for Attract."""
This is intended to be used by the CLI and unittests only, not tested
for live/production situations.
"""
import copy
import logging
import typing
from bson import ObjectId
from eve.methods.put import put_internal
@@ -108,3 +104,13 @@ def setup_for_attract(project_url, replace=False, svn_url=None):
log.info('Project %s was updated for Attract.', project_url)
return project
def set_task_types(project: dict, node_type_name: str, task_types: typing.List[str]):
eprops = project['extension_props']
attract_props = eprops[EXTENSION_NAME]
attract_props['task_types'][node_type_name] = task_types
log.info('Updating project %s, setting %s task_types to [%s]',
project['url'], node_type_name, ', '.join(task_types))
_update_project(project)

View File

@@ -178,3 +178,23 @@ function setHeaderCellsWidth(tableHeaderRowOriginal, tableHeaderRowFixed) {
return table_header.eq(i).width();
});
}
/* Returns a more-or-less reasonable message given an error response object. */
function xhrErrorResponseMessage(err) {
if (typeof err.responseJSON == 'undefined')
return err.statusText;
if (typeof err.responseJSON._error != 'undefined' && typeof err.responseJSON._error.message != 'undefined')
return err.responseJSON._error.message;
if (typeof err.responseJSON._message != 'undefined')
return err.responseJSON._message
return err.statusText;
}
function xhrErrorResponseElement(err, prefix) {
msg = xhrErrorResponseMessage(err);
return $('<span>')
.text(prefix + msg);
}

View File

@@ -0,0 +1,23 @@
| {% extends 'projects/edit_layout.html' %}
| {% set title = 'attract' %}
| {% block page_title %}Attract settings for {{ project.name }}{% endblock %}
| {% block head %}
script(src="{{ url_for('static_attract', filename='assets/js/generated/tutti.min.js') }}")
| {% endblock %}
| {% block project_context_header %}
span#project-edit-title
| {{ self.page_title() }}
| {% endblock %}
| {% block project_context %}
#node-edit-container
| {% block attract_container %}
| {% endblock attract_container %}
.settings-footer
p
| New to Attract?
| Sorry, we don't have documentation yet.
| {% endblock project_context %}

View File

@@ -0,0 +1,26 @@
| {% extends 'attract/project_settings/attract_layout.html' %}
| {% block attract_container %}
#node-edit-form
p This project is not setup for Attract (yet!)
p
button.btn.btn-success(onclick='setupForAttract()') Setup Project for Attract
| {% endblock attract_container %}
| {% block footer_scripts %}
script.
function setupForAttract() {
$.ajax({
url: '{{ url_for( "attract.setup_for_attract", project_url=project.url) }}',
method: 'POST',
})
.done(function() {
window.location.reload();
})
.fail(function(err) {
var err_elt = xhrErrorResponseElement(err, 'Error setting up your project: ');
toastr.error(err_elt);
});
}
| {% endblock %}

View File

@@ -0,0 +1,58 @@
| {% extends 'attract/project_settings/attract_layout.html' %}
| {% block attract_container %}
#node-edit-form
h3 Shot-related task types
p Here you can edit the task columns shown in this project's
=' '
a(href="{{ url_for('attract.shots.perproject.index', project_url=project['url']) }}") shot list
='.'
form(onsubmit="save(this, '{{ url_for('attract.save_task_types', project_url=project['url'], node_type_name=shot_node_type_name) }}'); return false;")
.input-group
textarea(
name="task_types",
type="text",
rows="{{ shot_task_types|count + 1 }}",
placeholder='Task types, one per line') {{ '\n'.join(shot_task_types) }}
.input-group
button.btn.btn-success.btn-block(type='submit')
i.pi-check
| Save Shot task types
h3 Asset-related task types
p Here you can edit the task columns shown in this project's
=' '
a(href="{{ url_for('attract.assets.perproject.index', project_url=project['url']) }}") asset list
='.'
form(onsubmit="save(this, '{{ url_for('attract.save_task_types', project_url=project['url'], node_type_name=asset_node_type_name) }}'); return false;")
.input-group
textarea(
name="task_types",
type="text",
rows="{{ asset_task_types|count + 1 }}",
placeholder='Task types, one per line') {{ '\n'.join(asset_task_types) }}
.input-group
button.btn.btn-success.btn-block(type='submit')
i.pi-check
| Save Asset task types
| {% endblock %}
| {% block footer_scripts %}
script.
function save(form, url) {
var $input = $(form).find('*[name="task_types"]');
var task_types = $input.val();
$.post(url, {'task_types': task_types})
.done(function(xhr) {
$input.val(xhr.task_types.join('\n'));
$input.attr('rows', xhr.task_types.length + 1);
toastr.success('Task types saved');
})
.fail(function(err) {
toastr.error(xhrErrorResponseElement(err, 'Error saving task types: '));
});
}
| {% endblock %}