diff --git a/attract/shot_manager.py b/attract/shot_manager.py index d2c9eae..10c8e64 100644 --- a/attract/shot_manager.py +++ b/attract/shot_manager.py @@ -1,5 +1,7 @@ """Shot management.""" +import collections + import attr import flask_login @@ -8,6 +10,7 @@ from pillar.web.system_util import pillar_api from . import attrs_extra from .node_types.shot import node_type_shot +from .node_types.task import node_type_task @attr.s @@ -38,3 +41,42 @@ class ShotManager(object): shot = pillarsdk.Node(node_props) shot.create(api=api) 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 diff --git a/attract/shots.py b/attract/shots.py index a11c0c6..fb8694b 100644 --- a/attract/shots.py +++ b/attract/shots.py @@ -36,14 +36,25 @@ def index(): def for_project(project, attract_props): api = pillar_api() - shots = pillarsdk.Node.all({ + found = pillarsdk.Node.all({ 'where': { 'project': project['_id'], 'node_type': node_type_shot['name'], }}, 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', - shots=shots['_items'], + shots=shots, + tasks_for_shots=tasks_for_shots, + task_types=task_types, project=project, attract_props=attract_props) diff --git a/attract/task_manager.py b/attract/task_manager.py index f82adca..8e8ada2 100644 --- a/attract/task_manager.py +++ b/attract/task_manager.py @@ -25,7 +25,7 @@ class TaskManager(object): 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. :rtype: pillarsdk.Node @@ -48,6 +48,8 @@ class TaskManager(object): if task_type: node_props['properties']['task_type'] = task_type + if parent: + node_props['parent'] = parent task = pillarsdk.Node(node_props) task.create(api=api) diff --git a/src/templates/attract/shots/for_project.jade b/src/templates/attract/shots/for_project.jade index a3fa627..a7e0f76 100644 --- a/src/templates/attract/shots/for_project.jade +++ b/src/templates/attract/shots/for_project.jade @@ -3,19 +3,29 @@ | {% block body %} #col_main h1 Shots for {{ project.name }} - table + table.table thead tr td Shot name - | {% for task_type in attract_props.task_types.attract_shot %} - td {{ task_type}} + | {% for task_type in task_types %} + td {{ task_type or '- other -' }} | {% 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 }} + | {% for task_type in task_types %} + 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 %} #col_right diff --git a/tests/test_shots.py b/tests/test_shots.py new file mode 100644 index 0000000..c3dfdd1 --- /dev/null +++ b/tests/test_shots.py @@ -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]) diff --git a/tests/test_tasks.py b/tests/test_tasks.py index b925244..ec3ba11 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -20,20 +20,20 @@ class TaskWorkflowTest(AbstractAttractTest): 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(): # Log in as project admin user pillar.auth.login_user(ctd.EXAMPLE_PROJECT_OWNER_ID) 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) return task @responses.activate def test_create_task(self): - task = self.create_task() + task = self.create_task(task_type=u'Just düüüh it') self.assertIsNotNone(task) # Test directly with MongoDB @@ -41,6 +41,12 @@ class TaskWorkflowTest(AbstractAttractTest): nodes_coll = self.app.data.driver.db['nodes'] found = nodes_coll.find_one(ObjectId(task['_id'])) 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 def test_edit_task(self):