diff --git a/attract/__init__.py b/attract/__init__.py index 88af0df..e3bcbc1 100644 --- a/attract/__init__.py +++ b/attract/__init__.py @@ -1,3 +1,5 @@ +import logging + from pillar.extension import PillarExtension from . import task_manager @@ -5,6 +7,7 @@ from . import task_manager class AttractExtension(PillarExtension): def __init__(self): + self._log = logging.getLogger('%s.AttractExtension' % __name__) self.task_manager = task_manager.TaskManager() @property diff --git a/attract/modules.py b/attract/modules.py index 6a846e7..fb369f7 100644 --- a/attract/modules.py +++ b/attract/modules.py @@ -1,7 +1,11 @@ +import functools import logging from flask import Blueprint, render_template + from pillar.api.utils import jsonify +from pillar.web.system_util import pillar_api +import pillarsdk blueprint = Blueprint('attract', __name__) log = logging.getLogger(__name__) @@ -30,3 +34,59 @@ def subversion_kick(): 'previous_last_seen_revision': last_seen_revision, 'last_seen_revision': observer.last_seen_revision, }) + + +def error_project_not_setup_for_attract(): + return render_template('attract/errors/project_not_setup.html') + + +def attract_project_view(extra_project_projections=None): + """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. + :type extra_project_projections: dict + """ + + if callable(extra_project_projections): + raise TypeError('Use with @attract_project_view() <-- note the parentheses') + + projections = { + '_id': 1, + 'name': 1, + 'node_types': 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): + api = pillar_api() + + project = pillarsdk.Project.find_by_url( + project_url, + {'projection': projections}, + api=api) + + node_type = project.get_node_type('attract.task') + if not node_type: + log.warning('createProject url=%s does not have node type attract.task', + project_url) + return error_project_not_setup_for_attract() + + return wrapped(project, *args, **kwargs) + + return wrapper + + return decorator diff --git a/attract/node_types/task.py b/attract/node_types/task.py index 9baad57..aa3f471 100644 --- a/attract/node_types/task.py +++ b/attract/node_types/task.py @@ -5,6 +5,7 @@ node_type_task = { 'status': { 'type': 'string', 'allowed': [ + 'invalid', 'todo', 'in_progress', 'on_hold', @@ -13,19 +14,10 @@ node_type_task = { 'final', 'review' ], + 'default': 'todo', 'required': True, }, - # Links to external systems (filenames, SVN repository URLs, SVN revisions, etc.) - 'external_links': { - 'svn_revisions': { - 'type': 'list', - 'schema': { - 'type': 'dict', - } - }, - }, - 'assigned_to': { 'type': 'dict', 'schema': { @@ -71,6 +63,5 @@ node_type_task = { 'time': {'visible': False}, }, - # TODO: is this None really needed? Check. - 'parent': [None, 'task', 'shot'] + 'parent': ['task', 'shot'], } diff --git a/attract/tasks.py b/attract/tasks.py index fe96fc2..d31ed4d 100644 --- a/attract/tasks.py +++ b/attract/tasks.py @@ -1,6 +1,13 @@ import logging from flask import Blueprint, render_template +import flask +import flask_login + +import pillarsdk +from pillar.web.system_util import pillar_api + +from .modules import attract_project_view blueprint = Blueprint('attract.tasks', __name__, url_prefix='/tasks') log = logging.getLogger(__name__) @@ -9,3 +16,51 @@ log = logging.getLogger(__name__) @blueprint.route('/') def index(): return render_template('attract/tasks/index.html') + + +@blueprint.route('//') +@attract_project_view() +def for_project(project): + api = pillar_api() + + tasks = pillarsdk.Node.all({ + 'project': project['_id'], + 'node_type': 'attract.task', + }, api=api) + + return render_template('attract/tasks/for_project.html', + tasks=tasks, + project=project) + + +@blueprint.route('//') +@attract_project_view() +def view_embed_task(project, task_id): + api = pillar_api() + + return 'Not done, come back later.' + + +@blueprint.route('//create') +@attract_project_view() +def create_task(project): + api = pillar_api() + + node_type = project.get_node_type('attract.task') + + node_props = dict( + name='New task', + project=project['_id'], + user=flask_login.current_user.objectid, + node_type=node_type['name'], + properties={ + 'status': node_type['dyn_schema']['status']['default'], + }, + ) + + task = pillarsdk.Node(node_props) + task.create(api=api) + + return flask.redirect(flask.url_for('attract.tasks.view_embed_task', + project_url=project['url'], + task_id=task['_id'])) diff --git a/src/templates/attract/errors/layout.jade b/src/templates/attract/errors/layout.jade new file mode 100644 index 0000000..8745da2 --- /dev/null +++ b/src/templates/attract/errors/layout.jade @@ -0,0 +1,27 @@ +doctype +html(lang="en") + head + meta(charset="utf-8") + title Attract - Blender Cloud + meta(name="viewport", content="width=device-width, initial-scale=1.0") + meta(name="description", content="Blender Cloud is a web based service developed by Blender Institute that allows people to access the training videos and all the data from the past open projects.") + meta(name="author", content="Blender Institute") + meta(name="theme-color", content="#3e92aa") + + script. + !function(e){"use strict";e.loadCSS=function(t,n,o){var r,i=e.document,l=i.createElement("link");if(n)r=n;else{var d=(i.body||i.getElementsByTagName("head")[0]).childNodes;r=d[d.length-1]}var a=i.styleSheets;l.rel="stylesheet",l.href=t,l.media="only x",r.parentNode.insertBefore(l,n?r:r.nextSibling);var f=function(e){for(var t=l.href,n=a.length;n--;)if(a[n].href===t)return e();setTimeout(function(){f(e)})};return l.onloadcssdefined=f,f(function(){l.media=o||"all"}),l},"undefined"!=typeof module&&(module.exports=e.loadCSS)}(this); + + loadCSS( "//fonts.googleapis.com/css?family=Roboto:300,400,500" ); + loadCSS( "//fonts.googleapis.com/css?family=Lato:300,400" ); + + link(href="{{ url_for('static_pillar', filename='assets/ico/favicon.png') }}", rel="shortcut icon") + link(href="{{ url_for('static_pillar', filename='assets/ico/apple-touch-icon-precomposed.png') }}", rel="icon apple-touch-icon-precomposed", sizes="192x192") + + link(href="{{ url_for('static_pillar', filename='assets/css/main.css') }}", rel="stylesheet") + + body.error + | {% block body %}{% endblock %} + + noscript + link(href='//fonts.googleapis.com/css?family=Roboto:300,400,500', rel='stylesheet', type='text/css') + link(href='//fonts.googleapis.com/css?family=Lato:300,400', rel='stylesheet', type='text/css') diff --git a/src/templates/attract/errors/project_not_setup.jade b/src/templates/attract/errors/project_not_setup.jade new file mode 100644 index 0000000..f813dc5 --- /dev/null +++ b/src/templates/attract/errors/project_not_setup.jade @@ -0,0 +1,12 @@ +| {% extends "attract/errors/layout.html" %} +| {% block body %} +#error_container.standalone + #error_box + .error-top-container + .error-title Project not set up for Attract. + .error-lead + p Currently Attract is in development, and only available to a select few. + hr + p If you want to use Attract, contact us at + a(href="mailto:cloudsupport@blender.institute") cloudsupport@blender.institute +| {% endblock %} diff --git a/src/templates/attract/tasks/for_project.jade b/src/templates/attract/tasks/for_project.jade new file mode 100644 index 0000000..2151881 --- /dev/null +++ b/src/templates/attract/tasks/for_project.jade @@ -0,0 +1,21 @@ +| {% extends 'attract/layout.html' %} +| {% block page_title %}Tasks{% endblock %} +| {% block body %} +#page-container + #page-header + .page-title + | Tasks for project {{ project.name }} + + #page-content + .page-triplet-container.homepage + .row + .col-md-4 + h2 The edit + p one column + .col-md-4 + h2 Tasks + a(href="{{ url_for('attract.tasks.index') }}") Go to task manager + .col-md-4 + h2 Other stuff + p three column +| {% endblock %} diff --git a/src/templates/attract/tasks/index.jade b/src/templates/attract/tasks/index.jade index 9a86b0b..ef7169c 100644 --- a/src/templates/attract/tasks/index.jade +++ b/src/templates/attract/tasks/index.jade @@ -1,5 +1,5 @@ | {% extends 'attract/layout.html' %} -| {% block page_title %}Attract{% endblock %} +| {% block page_title %}Tasks{% endblock %} | {% block body %} #page-container #page-header @@ -10,12 +10,9 @@ .page-triplet-container.homepage .row .col-md-4 - h2 The edit - p one column + h2 project 1 .col-md-4 - h2 Tasks - a(href="{{ url_for('attract.tasks.index') }}") Go to task manager + h2 project 2 .col-md-4 - h2 Other stuff - p three column + h2 project 3 | {% endblock %}