Added task creation per project and non-functional tasks-for-project view
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from pillar.extension import PillarExtension
|
from pillar.extension import PillarExtension
|
||||||
|
|
||||||
from . import task_manager
|
from . import task_manager
|
||||||
@@ -5,6 +7,7 @@ from . import task_manager
|
|||||||
|
|
||||||
class AttractExtension(PillarExtension):
|
class AttractExtension(PillarExtension):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self._log = logging.getLogger('%s.AttractExtension' % __name__)
|
||||||
self.task_manager = task_manager.TaskManager()
|
self.task_manager = task_manager.TaskManager()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
from pillar.api.utils import jsonify
|
from pillar.api.utils import jsonify
|
||||||
|
from pillar.web.system_util import pillar_api
|
||||||
|
import pillarsdk
|
||||||
|
|
||||||
blueprint = Blueprint('attract', __name__)
|
blueprint = Blueprint('attract', __name__)
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -30,3 +34,59 @@ def subversion_kick():
|
|||||||
'previous_last_seen_revision': last_seen_revision,
|
'previous_last_seen_revision': last_seen_revision,
|
||||||
'last_seen_revision': observer.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
|
||||||
|
@@ -5,6 +5,7 @@ node_type_task = {
|
|||||||
'status': {
|
'status': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'allowed': [
|
'allowed': [
|
||||||
|
'invalid',
|
||||||
'todo',
|
'todo',
|
||||||
'in_progress',
|
'in_progress',
|
||||||
'on_hold',
|
'on_hold',
|
||||||
@@ -13,19 +14,10 @@ node_type_task = {
|
|||||||
'final',
|
'final',
|
||||||
'review'
|
'review'
|
||||||
],
|
],
|
||||||
|
'default': 'todo',
|
||||||
'required': True,
|
'required': True,
|
||||||
},
|
},
|
||||||
|
|
||||||
# Links to external systems (filenames, SVN repository URLs, SVN revisions, etc.)
|
|
||||||
'external_links': {
|
|
||||||
'svn_revisions': {
|
|
||||||
'type': 'list',
|
|
||||||
'schema': {
|
|
||||||
'type': 'dict',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'assigned_to': {
|
'assigned_to': {
|
||||||
'type': 'dict',
|
'type': 'dict',
|
||||||
'schema': {
|
'schema': {
|
||||||
@@ -71,6 +63,5 @@ node_type_task = {
|
|||||||
'time': {'visible': False},
|
'time': {'visible': False},
|
||||||
},
|
},
|
||||||
|
|
||||||
# TODO: is this None really needed? Check.
|
'parent': ['task', 'shot'],
|
||||||
'parent': [None, 'task', 'shot']
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,13 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask import Blueprint, render_template
|
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')
|
blueprint = Blueprint('attract.tasks', __name__, url_prefix='/tasks')
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -9,3 +16,51 @@ log = logging.getLogger(__name__)
|
|||||||
@blueprint.route('/')
|
@blueprint.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template('attract/tasks/index.html')
|
return render_template('attract/tasks/index.html')
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route('/<project_url>/')
|
||||||
|
@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('/<project_url>/<task_id>')
|
||||||
|
@attract_project_view()
|
||||||
|
def view_embed_task(project, task_id):
|
||||||
|
api = pillar_api()
|
||||||
|
|
||||||
|
return 'Not done, come back later.'
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route('/<project_url>/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']))
|
||||||
|
27
src/templates/attract/errors/layout.jade
Normal file
27
src/templates/attract/errors/layout.jade
Normal file
@@ -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')
|
12
src/templates/attract/errors/project_not_setup.jade
Normal file
12
src/templates/attract/errors/project_not_setup.jade
Normal file
@@ -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 %}
|
21
src/templates/attract/tasks/for_project.jade
Normal file
21
src/templates/attract/tasks/for_project.jade
Normal file
@@ -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 %}
|
@@ -1,5 +1,5 @@
|
|||||||
| {% extends 'attract/layout.html' %}
|
| {% extends 'attract/layout.html' %}
|
||||||
| {% block page_title %}Attract{% endblock %}
|
| {% block page_title %}Tasks{% endblock %}
|
||||||
| {% block body %}
|
| {% block body %}
|
||||||
#page-container
|
#page-container
|
||||||
#page-header
|
#page-header
|
||||||
@@ -10,12 +10,9 @@
|
|||||||
.page-triplet-container.homepage
|
.page-triplet-container.homepage
|
||||||
.row
|
.row
|
||||||
.col-md-4
|
.col-md-4
|
||||||
h2 The edit
|
h2 project 1
|
||||||
p one column
|
|
||||||
.col-md-4
|
.col-md-4
|
||||||
h2 Tasks
|
h2 project 2
|
||||||
a(href="{{ url_for('attract.tasks.index') }}") Go to task manager
|
|
||||||
.col-md-4
|
.col-md-4
|
||||||
h2 Other stuff
|
h2 project 3
|
||||||
p three column
|
|
||||||
| {% endblock %}
|
| {% endblock %}
|
||||||
|
Reference in New Issue
Block a user