Files
attract/tests/test_shots.py

468 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- encoding: utf-8 -*-
import unittest
import responses
from bson import ObjectId
import pillarsdk
import pillarsdk.exceptions as sdk_exceptions
import pillar.tests
import pillar.auth
import pillar.tests.common_test_data as ctd
from abstract_attract_test import AbstractAttractTest
class AbstractShotTest(AbstractAttractTest):
def setUp(self, **kwargs):
AbstractAttractTest.setUp(self, **kwargs)
self.tmngr = self.attract.task_manager
self.smngr = self.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
class ShotManagerTest(AbstractShotTest):
@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, 'fx')
task2 = self.create_task(shot1_id, 'fx')
task3 = self.create_task(shot1_id, 'høken')
task4 = self.create_task(shot2_id, 'effects')
task5 = self.create_task(shot2_id, 'effects')
task6 = self.create_task(shot2_id, 'ï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_nodes([shot1, shot2],
['fx', 'høken', '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({
'fx': {task1['_id'], task2['_id']},
'høken': {task3['_id']},
}, shot_id_to_task[shot1_id])
self.assertEqual({
'effects': {task4['_id'], task5['_id']},
None: {task6['_id']},
}, shot_id_to_task[shot2_id])
@responses.activate
def test_edit_shot(self):
shot = self.create_shot()
pre_edit_shot = shot.to_dict()
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()
# No Etag checking, see T49555
# self.assertRaises(sdk_exceptions.PreconditionFailed,
# self.smngr.edit_shot,
# shot_id=shot['_id'],
# name=u'ผัดไทย',
# description=u'Shoot the Pad Thai',
# status='todo',
# _etag='jemoeder')
self.smngr.edit_shot(shot_id=shot['_id'],
name='ผัดไทย',
description='Shoot the Pad Thai',
status='todo',
notes=None,
_etag=shot._etag)
# Test directly with MongoDB
with self.app.test_request_context():
nodes_coll = self.app.data.driver.db['nodes']
found = nodes_coll.find_one(ObjectId(shot['_id']))
self.assertEqual(pre_edit_shot['name'], found['name']) # shouldn't be edited.
self.assertEqual('todo', found['properties']['status'])
self.assertEqual('Shoot the Pad Thai', found['description'])
self.assertNotIn('notes', found['properties'])
@responses.activate
def test_shot_summary(self):
shot1 = self.create_shot()
shot2 = self.create_shot()
shot3 = self.create_shot()
shot4 = self.create_shot()
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()
for shot, status in zip([shot1, shot2, shot3, shot4],
['todo', 'in_progress', 'todo', 'final']):
self.smngr.edit_shot(shot_id=shot['_id'],
status=status,
_etag=shot._etag)
# def shot_status_summary(self, project_id):
class PatchShotTest(AbstractShotTest):
@responses.activate
def test_patch_from_blender_happy(self):
shot = self.create_shot()
self.create_valid_auth_token(ctd.EXAMPLE_PROJECT_OWNER_ID, 'token')
url = '/api/nodes/%s' % shot._id
patch = {
'op': 'from-blender',
'$set': {
'name': '"shot" is "geschoten" in Dutch',
'properties.trim_start_in_frames': 123,
'properties.trim_end_in_frames': 0,
'properties.duration_in_edit_in_frames': 4215,
'properties.cut_in_timeline_in_frames': 1245,
'properties.status': 'on_hold',
}
}
self.patch(url, json=patch, auth_token='token')
dbnode = self.get(url, auth_token='token').json()
self.assertEqual('"shot" is "geschoten" in Dutch', dbnode['name'])
self.assertEqual(123, dbnode['properties']['trim_start_in_frames'])
self.assertEqual(0, dbnode['properties']['trim_end_in_frames'])
self.assertEqual('on_hold', dbnode['properties']['status'])
@responses.activate
def test_patch_activity(self):
"""Perform the edit, then check the resulting activity on the shot."""
shot = self.create_shot()
self.create_valid_auth_token(ctd.EXAMPLE_PROJECT_OWNER_ID, 'token')
url = '/api/nodes/%s' % shot._id
# Only change the name -- the activity should contain both the old and the new name.
old_name = shot['name']
new_name = '"shot" is "geschoten" in Dutch'
patch = {
'op': 'from-blender',
'$set': {
'name': new_name,
}
}
self.patch(url, json=patch, auth_token='token')
with self.app.test_request_context():
acts = self.attract.activities_for_node(shot._id)
self.assertEqual(2, acts['_meta']['total']) # Creation + edit
edit_act = acts['_items'][1]
self.assertIn(old_name, edit_act['verb'])
self.assertIn(new_name, edit_act['verb'])
pass
@responses.activate
def test_patch_from_web_happy(self):
shot = self.create_shot()
self.create_valid_auth_token(ctd.EXAMPLE_PROJECT_OWNER_ID, 'token')
url = '/api/nodes/%s' % shot._id
patch = {
'op': 'from-web',
'$set': {
'description': 'Таким образом, этот человек заходит в бар, и говорит…',
'properties.notes': 'Два бокала вашей лучшей водки, пожалуйста.',
'properties.status': 'final',
}
}
self.patch(url, json=patch, auth_token='token')
dbnode = self.get(url, auth_token='token').json()
self.assertEqual('Таким образом, этот человек заходит в бар, и говорит…',
dbnode['description'])
self.assertEqual('Два бокала вашей лучшей водки, пожалуйста.',
dbnode['properties']['notes'])
self.assertEqual('final', dbnode['properties']['status'])
self.assertEqual('New shot', dbnode['name'])
@responses.activate
def test_patch_from_web_happy_nones(self):
shot = self.create_shot()
self.create_valid_auth_token(ctd.EXAMPLE_PROJECT_OWNER_ID, 'token')
url = '/api/nodes/%s' % shot._id
patch = {
'op': 'from-web',
'$set': {
'description': None,
'properties.notes': None,
'properties.status': 'final',
}
}
self.patch(url, json=patch, auth_token='token')
dbnode = self.get(url, auth_token='token').json()
self.assertNotIn('description', dbnode)
self.assertNotIn('notes', dbnode['properties'])
self.assertEqual('final', dbnode['properties']['status'])
self.assertEqual('New shot', dbnode['name'])
@responses.activate
def test_patch_bad_op(self):
shot = self.create_shot()
self.create_valid_auth_token(ctd.EXAMPLE_PROJECT_OWNER_ID, 'token')
url = '/api/nodes/%s' % shot._id
patch = {'properties.status': 'todo'}
self.patch(url, json=patch, auth_token='token', expected_status=400)
@responses.activate
def test_patch_from_blender_bad_fields(self):
shot = self.create_shot()
self.create_valid_auth_token(ctd.EXAMPLE_PROJECT_OWNER_ID, 'token')
url = '/api/nodes/%s' % shot._id
patch = {
'op': 'from-blender',
'$set': {
'invalid.property': 'JE MOEDER',
}
}
self.patch(url, json=patch, auth_token='token', expected_status=400)
@responses.activate
def test_patch_from_blender_bad_status(self):
shot = self.create_shot()
self.create_valid_auth_token(ctd.EXAMPLE_PROJECT_OWNER_ID, 'token')
url = '/api/nodes/%s' % shot._id
patch = {
'op': 'from-blender',
'$set': {
'properties.status': 'JE MOEDER',
}
}
self.patch(url, json=patch, auth_token='token', expected_status=422)
@responses.activate
def test_patch_unauthenticated(self):
shot = self.create_shot()
url = '/api/nodes/%s' % shot._id
patch = {
'op': 'from-blender',
'$set': {
'properties.status': 'in_progress',
}
}
self.patch(url, json=patch, expected_status=403)
@responses.activate
def test_patch_bad_user(self):
shot = self.create_shot()
self.create_user(24 * 'a')
self.create_valid_auth_token(24 * 'a', 'other')
url = '/api/nodes/%s' % shot._id
patch = {
'op': 'from-blender',
'$set': {
'properties.status': 'in_progress',
}
}
self.patch(url, json=patch, auth_token='other', expected_status=403)
@responses.activate
def test_patch_unlink(self):
shot = self.create_shot()
self.create_valid_auth_token(ctd.EXAMPLE_PROJECT_OWNER_ID, 'token')
url = '/api/nodes/%s' % shot._id
dbnode = self.get(url, auth_token='token').json()
self.assertTrue(dbnode['properties']['used_in_edit'])
patch = {'op': 'unlink'}
self.patch(url, json=patch, auth_token='token')
dbnode = self.get(url, auth_token='token').json()
self.assertFalse(dbnode['properties']['used_in_edit'])
@responses.activate
def test_patch_unlink_deleted(self):
"""Unlinking a deleted shot shouldn't undelete it.
We implement PATCH by changing then PUTing, which undeletes by default.
"""
shot = self.create_shot()
self.create_valid_auth_token(ctd.EXAMPLE_PROJECT_OWNER_ID, 'token')
url = '/api/nodes/%s' % shot._id
# Delete (and verify deletion)
self.delete(url, auth_token='token',
headers={'If-Match': shot['_etag']},
expected_status=204)
self.get(url, auth_token='token', expected_status=404)
patch = {'op': 'unlink'}
self.patch(url, json=patch, auth_token='token')
self.get(url, auth_token='token', expected_status=404)
@responses.activate
def test_patch_relink(self):
shot = self.create_shot()
self.create_valid_auth_token(ctd.EXAMPLE_PROJECT_OWNER_ID, 'token')
url = '/api/nodes/%s' % shot._id
self.patch(url, json={'op': 'unlink'}, auth_token='token')
dbnode = self.get(url, auth_token='token').json()
self.assertFalse(dbnode['properties']['used_in_edit'])
self.patch(url, json={'op': 'relink'}, auth_token='token')
dbnode = self.get(url, auth_token='token').json()
self.assertTrue(dbnode['properties']['used_in_edit'])
@responses.activate
def test_patch_relink_deleted(self):
"""Relinking a deleted shot should undelete it.
We implement PATCH by changing then PUTing, which undeletes.
"""
shot = self.create_shot()
self.create_valid_auth_token(ctd.EXAMPLE_PROJECT_OWNER_ID, 'token')
url = '/api/nodes/%s' % shot._id
# Delete (and verify deletion)
self.delete(url, auth_token='token',
headers={'If-Match': shot['_etag']},
expected_status=204)
self.get(url, auth_token='token', expected_status=404)
patch = {'op': 'relink'}
self.patch(url, json=patch, auth_token='token')
dbnode = self.get(url, auth_token='token').json()
self.assertTrue(dbnode['properties']['used_in_edit'])
class RequiredAfterCreationTest(AbstractShotTest):
"""
This tests Pillar stuff, but requires attract_shot since that's what the
required_after_creation=False was created for.
Placing the test here was easier than creating a node type in Pillar
specifically for this test case. Once we use that validator in Pillar
itself, we can move this test there too.
"""
def test_create_shot(self):
from attract.node_types import node_type_shot
self.user_id = self.create_project_admin(self.project)
self.create_valid_auth_token(self.user_id, 'token')
node_type_name = node_type_shot['name']
shot = {'name': 'test shot',
'description': '',
'properties': {'trim_start_in_frames': 0,
'trim_end_in_frames': 0,
'duration_in_edit_in_frames': 1,
'cut_in_timeline_in_frames': 0},
'node_type': node_type_name,
'project': str(self.proj_id),
'user': str(self.user_id)}
resp = self.post('/api/nodes', json=shot,
auth_token='token', expected_status=201)
info = resp.json()
resp = self.get('/api/nodes/%(_id)s' % info, auth_token='token')
json_shot = resp.json()
self.assertEqual(node_type_shot['dyn_schema']['status']['default'],
json_shot['properties']['status'])
return json_shot
# TODO: should test editing a shot as well, but I had issues with the PillarSDK
# not handling deleting of properties.
class ProjectSummaryTest(unittest.TestCase):
def setUp(self):
from attract.shots_and_assets import ProjectSummary
self.summ = ProjectSummary()
self.summ.count('todo')
self.summ.count('todo')
self.summ.count('in-progress')
self.summ.count('überhard')
self.summ.count('Æon Flux')
self.summ.count('Æon Flux')
self.summ.count('in-progress')
self.summ.count('todo')
def test_counting(self):
self.assertEqual(8, self.summ._total)
self.assertEqual(3, self.summ._counts['todo'])
self.assertEqual(2, self.summ._counts['Æon Flux'])
def test_percentages(self):
percs = list(self.summ.percentages())
self.assertEqual(('in-progress', 25), percs[0])
self.assertEqual(('todo', 38), percs[1])
self.assertEqual(('Æon Flux', 25), percs[2])
# This should be rounded down, not rounded up, to ensure the sum of
# percentages is 100.
self.assertEqual(('überhard', 12), percs[3])