Added shot overview with tasks.
javascript links to open or create tasks aren't implemented yet.
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
"""Shot management."""
|
"""Shot management."""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
import flask_login
|
import flask_login
|
||||||
|
|
||||||
@@ -8,6 +10,7 @@ from pillar.web.system_util import pillar_api
|
|||||||
|
|
||||||
from . import attrs_extra
|
from . import attrs_extra
|
||||||
from .node_types.shot import node_type_shot
|
from .node_types.shot import node_type_shot
|
||||||
|
from .node_types.task import node_type_task
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
@@ -38,3 +41,42 @@ class ShotManager(object):
|
|||||||
shot = pillarsdk.Node(node_props)
|
shot = pillarsdk.Node(node_props)
|
||||||
shot.create(api=api)
|
shot.create(api=api)
|
||||||
return shot
|
return shot
|
||||||
|
|
||||||
|
def tasks_for_shots(self, shots, known_task_types):
|
||||||
|
"""Returns a dict of tasks for each shot.
|
||||||
|
|
||||||
|
:param shots: list of shot nodes.
|
||||||
|
:param known_task_types: Collection of task type names. Any task with a
|
||||||
|
type not in this list will map the None key.
|
||||||
|
:returns: a dict {shot id: tasks}, where tasks is a dict in which the keys are the
|
||||||
|
task types, and the values are sets of tasks of that type.
|
||||||
|
:rtype: dict
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
api = pillar_api()
|
||||||
|
|
||||||
|
id_to_shot = {}
|
||||||
|
shot_id_to_tasks = {}
|
||||||
|
for shot in shots:
|
||||||
|
shot_id = shot['_id']
|
||||||
|
id_to_shot[shot_id] = shot
|
||||||
|
shot_id_to_tasks[shot_id] = collections.defaultdict(set)
|
||||||
|
|
||||||
|
found = pillarsdk.Node.all({
|
||||||
|
'where': {
|
||||||
|
'node_type': node_type_task['name'],
|
||||||
|
'parent': {'$in': list(id_to_shot.keys())},
|
||||||
|
}
|
||||||
|
}, api=api)
|
||||||
|
|
||||||
|
known = set(known_task_types) # for fast lookups
|
||||||
|
|
||||||
|
# Now put the tasks into the right spot.
|
||||||
|
for task in found['_items']:
|
||||||
|
task_type = task.properties.task_type
|
||||||
|
if task_type not in known:
|
||||||
|
task_type = None
|
||||||
|
shot_id_to_tasks[task.parent][task_type].add(task)
|
||||||
|
|
||||||
|
return shot_id_to_tasks
|
||||||
|
@@ -36,14 +36,25 @@ def index():
|
|||||||
def for_project(project, attract_props):
|
def for_project(project, attract_props):
|
||||||
api = pillar_api()
|
api = pillar_api()
|
||||||
|
|
||||||
shots = pillarsdk.Node.all({
|
found = pillarsdk.Node.all({
|
||||||
'where': {
|
'where': {
|
||||||
'project': project['_id'],
|
'project': project['_id'],
|
||||||
'node_type': node_type_shot['name'],
|
'node_type': node_type_shot['name'],
|
||||||
}}, api=api)
|
}}, api=api)
|
||||||
|
shots = found['_items']
|
||||||
|
|
||||||
|
tasks_for_shots = current_attract.shot_manager.tasks_for_shots(
|
||||||
|
shots,
|
||||||
|
attract_props.task_types.attract_shot,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Append the task type onto which 'other' tasks are mapped.
|
||||||
|
task_types = attract_props.task_types.attract_shot + [None]
|
||||||
|
|
||||||
return render_template('attract/shots/for_project.html',
|
return render_template('attract/shots/for_project.html',
|
||||||
shots=shots['_items'],
|
shots=shots,
|
||||||
|
tasks_for_shots=tasks_for_shots,
|
||||||
|
task_types=task_types,
|
||||||
project=project,
|
project=project,
|
||||||
attract_props=attract_props)
|
attract_props=attract_props)
|
||||||
|
|
||||||
|
@@ -25,7 +25,7 @@ class TaskManager(object):
|
|||||||
|
|
||||||
self._log.info("Task '%s' logged in SVN: %s", task_id, log_entry)
|
self._log.info("Task '%s' logged in SVN: %s", task_id, log_entry)
|
||||||
|
|
||||||
def create_task(self, project, task_type=None):
|
def create_task(self, project, task_type=None, parent=None):
|
||||||
"""Creates a new task, owned by the current user.
|
"""Creates a new task, owned by the current user.
|
||||||
|
|
||||||
:rtype: pillarsdk.Node
|
:rtype: pillarsdk.Node
|
||||||
@@ -48,6 +48,8 @@ class TaskManager(object):
|
|||||||
|
|
||||||
if task_type:
|
if task_type:
|
||||||
node_props['properties']['task_type'] = task_type
|
node_props['properties']['task_type'] = task_type
|
||||||
|
if parent:
|
||||||
|
node_props['parent'] = parent
|
||||||
|
|
||||||
task = pillarsdk.Node(node_props)
|
task = pillarsdk.Node(node_props)
|
||||||
task.create(api=api)
|
task.create(api=api)
|
||||||
|
@@ -3,19 +3,29 @@
|
|||||||
| {% block body %}
|
| {% block body %}
|
||||||
#col_main
|
#col_main
|
||||||
h1 Shots for <em>{{ project.name }}</em>
|
h1 Shots for <em>{{ project.name }}</em>
|
||||||
table
|
table.table
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
td Shot name
|
td Shot name
|
||||||
| {% for task_type in attract_props.task_types.attract_shot %}
|
| {% for task_type in task_types %}
|
||||||
td {{ task_type}}
|
td {{ task_type or '- other -' }}
|
||||||
| {% endfor %}
|
| {% endfor %}
|
||||||
tbody
|
tbody
|
||||||
| {% for shot in shots %}
|
| {% for shot in shots %}
|
||||||
tr
|
tr
|
||||||
td {{ shot.name }}
|
td {{ shot.name }}
|
||||||
| {% for task_type in attract_props.task_types.attract_shot %}
|
| {% for task_type in task_types %}
|
||||||
td tasks of type {{ task_type }}
|
td
|
||||||
|
| {% for task in tasks_for_shots[shot._id][task_type] %}
|
||||||
|
a(
|
||||||
|
href="javascript:task_open('{{ task._id }}');",
|
||||||
|
class="status-{{ task.properties.status }}") {{ task.name }}
|
||||||
|
br
|
||||||
|
| {% endfor %}
|
||||||
|
a(
|
||||||
|
href="javascript:create_task('{{ shot._id }}', '{{ task_type }}');")
|
||||||
|
| Create task
|
||||||
|
br
|
||||||
| {% endfor %}
|
| {% endfor %}
|
||||||
| {% endfor %}
|
| {% endfor %}
|
||||||
#col_right
|
#col_right
|
||||||
|
84
tests/test_shots.py
Normal file
84
tests/test_shots.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
import responses
|
||||||
|
|
||||||
|
import pillarsdk
|
||||||
|
import pillar.tests
|
||||||
|
import pillar.auth
|
||||||
|
import pillar.tests.common_test_data as ctd
|
||||||
|
|
||||||
|
from abstract_attract_test import AbstractAttractTest
|
||||||
|
|
||||||
|
|
||||||
|
class ShotManagerTest(AbstractAttractTest):
|
||||||
|
def setUp(self, **kwargs):
|
||||||
|
AbstractAttractTest.setUp(self, **kwargs)
|
||||||
|
|
||||||
|
self.tmngr = self.app.pillar_extensions['attract'].task_manager
|
||||||
|
self.smngr = self.app.pillar_extensions['attract'].shot_manager
|
||||||
|
|
||||||
|
self.proj_id, self.project = self.ensure_project_exists()
|
||||||
|
|
||||||
|
self.sdk_project = pillarsdk.Project(pillar.tests.mongo_to_sdk(self.project))
|
||||||
|
|
||||||
|
def create_task(self, shot_id, task_type):
|
||||||
|
with self.app.test_request_context():
|
||||||
|
# Log in as project admin user
|
||||||
|
pillar.auth.login_user(ctd.EXAMPLE_PROJECT_OWNER_ID)
|
||||||
|
|
||||||
|
self.mock_blenderid_validate_happy()
|
||||||
|
task = self.tmngr.create_task(self.sdk_project, parent=shot_id, task_type=task_type)
|
||||||
|
|
||||||
|
self.assertIsInstance(task, pillarsdk.Node)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def create_shot(self):
|
||||||
|
with self.app.test_request_context():
|
||||||
|
# Log in as project admin user
|
||||||
|
pillar.auth.login_user(ctd.EXAMPLE_PROJECT_OWNER_ID)
|
||||||
|
|
||||||
|
self.mock_blenderid_validate_happy()
|
||||||
|
shot = self.smngr.create_shot(self.sdk_project)
|
||||||
|
|
||||||
|
self.assertIsInstance(shot, pillarsdk.Node)
|
||||||
|
return shot
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_tasks_for_shot(self):
|
||||||
|
shot1 = self.create_shot()
|
||||||
|
shot2 = self.create_shot()
|
||||||
|
|
||||||
|
shot1_id = shot1['_id']
|
||||||
|
shot2_id = shot2['_id']
|
||||||
|
|
||||||
|
task1 = self.create_task(shot1_id, u'fx')
|
||||||
|
task2 = self.create_task(shot1_id, u'fx')
|
||||||
|
task3 = self.create_task(shot1_id, u'høken')
|
||||||
|
|
||||||
|
task4 = self.create_task(shot2_id, u'effects')
|
||||||
|
task5 = self.create_task(shot2_id, u'effects')
|
||||||
|
task6 = self.create_task(shot2_id, u'ïnžane')
|
||||||
|
|
||||||
|
with self.app.test_request_context():
|
||||||
|
# Log in as project admin user
|
||||||
|
pillar.auth.login_user(ctd.EXAMPLE_PROJECT_OWNER_ID)
|
||||||
|
|
||||||
|
self.mock_blenderid_validate_happy()
|
||||||
|
shot_id_to_task = self.smngr.tasks_for_shots([shot1, shot2],
|
||||||
|
[u'fx', u'høken', u'effects'])
|
||||||
|
|
||||||
|
# Just test based on task IDs, as strings are turned into datetimes etc. by the API,
|
||||||
|
# so we can't test equality.
|
||||||
|
for all_tasks in shot_id_to_task.values():
|
||||||
|
for task_type, tasks in all_tasks.items():
|
||||||
|
all_tasks[task_type] = {task['_id'] for task in tasks}
|
||||||
|
|
||||||
|
self.assertEqual({
|
||||||
|
u'fx': {task1['_id'], task2['_id']},
|
||||||
|
u'høken': {task3['_id']},
|
||||||
|
}, shot_id_to_task[shot1_id])
|
||||||
|
|
||||||
|
self.assertEqual({
|
||||||
|
u'effects': {task4['_id'], task5['_id']},
|
||||||
|
None: {task6['_id']},
|
||||||
|
}, shot_id_to_task[shot2_id])
|
@@ -20,20 +20,20 @@ class TaskWorkflowTest(AbstractAttractTest):
|
|||||||
|
|
||||||
self.sdk_project = pillarsdk.Project(pillar.tests.mongo_to_sdk(self.project))
|
self.sdk_project = pillarsdk.Project(pillar.tests.mongo_to_sdk(self.project))
|
||||||
|
|
||||||
def create_task(self):
|
def create_task(self, task_type=None):
|
||||||
with self.app.test_request_context():
|
with self.app.test_request_context():
|
||||||
# Log in as project admin user
|
# Log in as project admin user
|
||||||
pillar.auth.login_user(ctd.EXAMPLE_PROJECT_OWNER_ID)
|
pillar.auth.login_user(ctd.EXAMPLE_PROJECT_OWNER_ID)
|
||||||
|
|
||||||
self.mock_blenderid_validate_happy()
|
self.mock_blenderid_validate_happy()
|
||||||
task = self.mngr.create_task(self.sdk_project)
|
task = self.mngr.create_task(self.sdk_project, task_type=task_type)
|
||||||
|
|
||||||
self.assertIsInstance(task, pillarsdk.Node)
|
self.assertIsInstance(task, pillarsdk.Node)
|
||||||
return task
|
return task
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_create_task(self):
|
def test_create_task(self):
|
||||||
task = self.create_task()
|
task = self.create_task(task_type=u'Just düüüh it')
|
||||||
self.assertIsNotNone(task)
|
self.assertIsNotNone(task)
|
||||||
|
|
||||||
# Test directly with MongoDB
|
# Test directly with MongoDB
|
||||||
@@ -41,6 +41,12 @@ class TaskWorkflowTest(AbstractAttractTest):
|
|||||||
nodes_coll = self.app.data.driver.db['nodes']
|
nodes_coll = self.app.data.driver.db['nodes']
|
||||||
found = nodes_coll.find_one(ObjectId(task['_id']))
|
found = nodes_coll.find_one(ObjectId(task['_id']))
|
||||||
self.assertIsNotNone(found)
|
self.assertIsNotNone(found)
|
||||||
|
self.assertEqual(u'Just düüüh it', found['properties']['task_type'])
|
||||||
|
|
||||||
|
# Test it through the API
|
||||||
|
resp = self.get('/api/nodes/%s' % task['_id'])
|
||||||
|
found = resp.json()
|
||||||
|
self.assertEqual(u'Just düüüh it', found['properties']['task_type'])
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_edit_task(self):
|
def test_edit_task(self):
|
||||||
|
Reference in New Issue
Block a user