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:
@@ -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."""
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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);
|
||||
}
|
||||
|
23
src/templates/attract/project_settings/attract_layout.jade
Normal file
23
src/templates/attract/project_settings/attract_layout.jade
Normal 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 %}
|
26
src/templates/attract/project_settings/offer_setup.jade
Normal file
26
src/templates/attract/project_settings/offer_setup.jade
Normal 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 %}
|
58
src/templates/attract/project_settings/settings.jade
Normal file
58
src/templates/attract/project_settings/settings.jade
Normal 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 %}
|
Reference in New Issue
Block a user