diff --git a/attract/__init__.py b/attract/__init__.py index 12388f4..355718a 100644 --- a/attract/__init__.py +++ b/attract/__init__.py @@ -5,6 +5,7 @@ from werkzeug.local import LocalProxy from pillar.extension import PillarExtension import attract.task_manager +import attract.shot_manager EXTENSION_NAME = 'attract' @@ -13,6 +14,7 @@ class AttractExtension(PillarExtension): def __init__(self): self._log = logging.getLogger('%s.AttractExtension' % __name__) self.task_manager = attract.task_manager.TaskManager() + self.shot_manager = attract.shot_manager.ShotManager() @property def name(self): @@ -52,12 +54,14 @@ class AttractExtension(PillarExtension): :rtype: list of flask.Blueprint objects. """ - from . import modules, tasks + from . import modules, tasks, shots return [ modules.blueprint, tasks.blueprint, tasks.perproject_blueprint, + shots.blueprint, + shots.perproject_blueprint, ] @property @@ -78,12 +82,11 @@ class AttractExtension(PillarExtension): subversion.task_logged.connect(self.task_manager.task_logged_in_svn) -def _get_task_manager(): - """Returns the Attract task manager of the current application.""" +def _get_current_attract(): + """Returns the Attract extension of the current application.""" - current_attract = flask.current_app.pillar_extensions[EXTENSION_NAME] - return current_attract.task_manager + return flask.current_app.pillar_extensions[EXTENSION_NAME] -current_task_manager = LocalProxy(_get_task_manager) -"""Attract Task manager of the current app.""" +current_attract = LocalProxy(_get_current_attract) +"""Attract extension of the current app.""" diff --git a/attract/node_types/shot.py b/attract/node_types/shot.py index 8990fa8..3c15e59 100644 --- a/attract/node_types/shot.py +++ b/attract/node_types/shot.py @@ -23,6 +23,7 @@ node_type_shot = { 'review', 'final' ], + 'default': 'todo', }, 'notes': { 'type': 'string', diff --git a/attract/shot_manager.py b/attract/shot_manager.py new file mode 100644 index 0000000..d2c9eae --- /dev/null +++ b/attract/shot_manager.py @@ -0,0 +1,40 @@ +"""Shot management.""" + +import attr +import flask_login + +import pillarsdk +from pillar.web.system_util import pillar_api + +from . import attrs_extra +from .node_types.shot import node_type_shot + + +@attr.s +class ShotManager(object): + _log = attrs_extra.log('%s.ShotManager' % __name__) + + def create_shot(self, project): + """Creates a new shot, owned by the current user. + + :rtype: pillarsdk.Node + """ + + api = pillar_api() + node_type = project.get_node_type(node_type_shot['name']) + if not node_type: + raise ValueError('Project %s not set up for Attract' % project._id) + + node_props = dict( + name='New shot', + project=project['_id'], + user=flask_login.current_user.objectid, + node_type=node_type['name'], + properties={ + 'status': node_type['dyn_schema']['status']['default'], + }, + ) + + shot = pillarsdk.Node(node_props) + shot.create(api=api) + return shot diff --git a/attract/shots.py b/attract/shots.py new file mode 100644 index 0000000..a11c0c6 --- /dev/null +++ b/attract/shots.py @@ -0,0 +1,75 @@ +import logging + +from flask import Blueprint, render_template, request +import flask + +import pillarsdk +from pillar.web.system_util import pillar_api + +from .modules import attract_project_view +from .node_types.shot import node_type_shot +from . import current_attract + +blueprint = Blueprint('attract.shots', __name__, url_prefix='/shots') +perproject_blueprint = Blueprint('attract.shots.perproject', __name__, + url_prefix='//shots') +log = logging.getLogger(__name__) + + +@blueprint.route('/') +def index(): + api = pillar_api() + + # Find projects that are set up for Attract. + projects = pillarsdk.Project.all({ + 'where': { + 'extension_props.attract': {'$exists': 1}, + 'node_types.name': node_type_shot['name'], + }}, api=api) + + return render_template('attract/shots/index.html', + projects=projects['_items']) + + +@perproject_blueprint.route('/', endpoint='index') +@attract_project_view(extension_props=True) +def for_project(project, attract_props): + api = pillar_api() + + shots = pillarsdk.Node.all({ + 'where': { + 'project': project['_id'], + 'node_type': node_type_shot['name'], + }}, api=api) + + return render_template('attract/shots/for_project.html', + shots=shots['_items'], + project=project, + attract_props=attract_props) + + +@perproject_blueprint.route('/') +@attract_project_view(extension_props=True) +def view_shot(project, attract_props, shot_id): + api = pillar_api() + + shot = pillarsdk.Node.find(shot_id, api=api) + + return render_template('attract/shots/shot.html', + shot=shot, + project=project, + attract_props=attract_props) + + +# TODO: remove GET method once Pablo has made a proper button to call this URL with a POST. +@perproject_blueprint.route('/create', methods=['POST', 'GET']) +@attract_project_view() +def create_shot(project): + shot = current_attract.shot_manager.create_shot(project) + + resp = flask.make_response() + resp.headers['Location'] = flask.url_for('.view_shot', + project_url=project['url'], + shot_id=shot['_id']) + resp.status_code = 201 + return resp diff --git a/attract/task_manager.py b/attract/task_manager.py index f27bc1d..cdf0b60 100644 --- a/attract/task_manager.py +++ b/attract/task_manager.py @@ -7,6 +7,7 @@ import pillarsdk from pillar.web.system_util import pillar_api from . import attrs_extra +from .node_types.shot import node_type_shot @attr.s @@ -31,7 +32,7 @@ class TaskManager(object): """ api = pillar_api() - node_type = project.get_node_type('attract_task') + node_type = project.get_node_type(node_type_shot['name']) if not node_type: raise ValueError('Project %s not set up for Attract' % project._id) diff --git a/attract/tasks.py b/attract/tasks.py index 9c64c33..ae8dcd2 100644 --- a/attract/tasks.py +++ b/attract/tasks.py @@ -8,7 +8,7 @@ from pillar.web.system_util import pillar_api from .modules import attract_project_view from .node_types.task import node_type_task -from . import current_task_manager +from . import current_attract blueprint = Blueprint('attract.tasks', __name__, url_prefix='/tasks') perproject_blueprint = Blueprint('attract.tasks.perproject', __name__, @@ -21,7 +21,7 @@ def index(): return render_template('attract/tasks/index.html') -@perproject_blueprint.route('/') +@perproject_blueprint.route('/', endpoint='index') @attract_project_view() def for_project(project): api = pillar_api() @@ -59,7 +59,7 @@ def save(project, task_id): log.info('Saving task %s', task_id) log.debug('Form data: %s', request.form) - task = current_task_manager.edit_task(task_id, **request.form.to_dict()) + task = current_attract.task_manager.edit_task(task_id, **request.form.to_dict()) return flask.jsonify({'task_id': task_id, 'etag': task._etag, 'time': task._updated }) @@ -69,7 +69,7 @@ def save(project, task_id): @perproject_blueprint.route('/create/', methods=['POST', 'GET']) @attract_project_view() def create_task(project, task_type=None): - task = current_task_manager.create_task(project, task_type=task_type) + task = current_attract.task_manager.create_task(project, task_type=task_type) resp = flask.make_response() resp.headers['Location'] = flask.url_for('attract.tasks.view_embed_task', diff --git a/src/templates/attract/shots/for_project.jade b/src/templates/attract/shots/for_project.jade new file mode 100644 index 0000000..a94668d --- /dev/null +++ b/src/templates/attract/shots/for_project.jade @@ -0,0 +1,30 @@ +| {% extends 'attract/layout.html' %} +| {% block page_title %}Shots{% endblock %} +| {% block body %} +#page-container + #page-header + .page-title + | Shots for project {{ project.name }} + + #page-content + .page-triplet-container.homepage + .row + .col-md-8 + table + thead + tr + td Shot name + | {% for task_type in attract_props.task_types.attract_shot %} + td {{ task_type}} + | {% endfor %} + tbody + | {% for shot in shots %} + tr + td {{ shot.name }} + | {% for task_type in attract_props.task_types.attract_shot %} + td tasks of type {{ task_type }} + | {% endfor %} + | {% endfor %} + .col-md-4 + .well Task details here +| {% endblock %} diff --git a/src/templates/attract/shots/index.jade b/src/templates/attract/shots/index.jade new file mode 100644 index 0000000..c5d6f09 --- /dev/null +++ b/src/templates/attract/shots/index.jade @@ -0,0 +1,23 @@ +| {% extends 'attract/layout.html' %} +| {% block page_title %}Shots{% endblock %} +| {% block body %} +#page-container + #page-header + .page-title + | Attract - Shots + + #page-content + .page-triplet-container.homepage + .row + .col-md-4 + h2 Attract projects + ul + | {% for project in projects %} + li + a(href="{{ url_for('attract.shots.perproject.index', project_url=project.url) }}") {{ project.name }} + | {% endfor %} + .col-md-4 + h2 project 2 + .col-md-4 + h2 project 3 +| {% endblock %} diff --git a/src/templates/attract/shots/shot.jade b/src/templates/attract/shots/shot.jade new file mode 100644 index 0000000..f8713be --- /dev/null +++ b/src/templates/attract/shots/shot.jade @@ -0,0 +1,16 @@ +| {% extends 'attract/layout.html' %} +| {% block page_title %}Shot {{ shot.name }}{% endblock %} +| {% block body %} +#page-container + #page-header + .page-title + | Shot {{ shot.name }} + + #page-content + .page-triplet-container.homepage + .row + .col-md-8 + p this is a shot. + .col-md-4 + .well Task details here +| {% endblock %}