diff --git a/tests/base.py b/tests/base.py index 2920d38f2d..6eba2d3934 100644 --- a/tests/base.py +++ b/tests/base.py @@ -371,14 +371,16 @@ def generate_fixture_episode(self, name="E01", project_id=None): ) return self.episode - def generate_fixture_shot(self, name="P01", nb_frames=0): + def generate_fixture_shot(self, name="P01", nb_frames=0, sequence_id=None): + if sequence_id is None: + sequence_id = self.sequence.id self.shot = Entity.create( name=name, description="Description Shot 01", data={"fps": 25, "frame_in": 0, "frame_out": 100}, project_id=self.project.id, entity_type_id=self.shot_type.id, - parent_id=self.sequence.id, + parent_id=sequence_id, nb_frames=nb_frames, ) return self.shot @@ -717,16 +719,24 @@ def generate_fixture_task_standard(self): self.project.save() return self.task_standard - def generate_fixture_shot_task(self, name="Master", task_type_id=None): + def generate_fixture_shot_task( + self, + name="Master", + shot_id=None, + task_type_id=None + ): if task_type_id is None: task_type_id = self.task_type_animation.id + if shot_id is None: + shot_id = self.shot.id + self.shot_task = Task.create( name=name, project_id=self.project.id, task_type_id=task_type_id, task_status_id=self.task_status.id, - entity_id=self.shot.id, + entity_id=shot_id, assignees=[self.person], assigner_id=self.assigner.id, ) @@ -907,18 +917,26 @@ def generate_fixture_organisation(self): ) def generate_fixture_preview_file( - self, revision=1, name="main", position=1, status="ready" + self, + revision=1, + name="main", + position=1, + status="ready", + duration=10, + task_id=None, ): + task_id = task_id or self.task.id self.preview_file = PreviewFile.create( name=name, revision=revision, description="test description", source="pytest", - task_id=self.task.id, + task_id=task_id, extension="mp4", person_id=self.person.id, position=position, status=status, + duration=duration, ) return self.preview_file @@ -1069,6 +1087,89 @@ def generate_shot_suite(self): self.generate_fixture_shot() self.generate_fixture_scene() + def generate_fixture_shot_tasks_and_previews( + self, + task_type_id + ): + episode_01 = self.episode + sequence_01 = self.sequence + shot_01 = self.shot + shot_02 = self.generate_fixture_shot("SH02") + shot_03 = self.generate_fixture_shot("SH03") + + self.generate_fixture_episode("E02") + episode_02 = self.episode + self.generate_fixture_sequence("S02", episode_id=episode_02.id) + sequence_02 = self.sequence + shot_e201 = self.generate_fixture_shot( + "E2SH01", sequence_id=sequence_02.id + ) + + task_shot_01 = self.generate_fixture_shot_task( + shot_id=shot_01.id, + task_type_id=task_type_id + ) + task_shot_02 = self.generate_fixture_shot_task( + shot_id=shot_02.id, + task_type_id=task_type_id + ) + task_shot_03 = self.generate_fixture_shot_task( + shot_id=shot_03.id, + task_type_id=task_type_id + ) + task_shot_e201 = self.generate_fixture_shot_task( + shot_id=shot_e201.id, + task_type_id=task_type_id + ) + preview_01 = self.generate_fixture_preview_file( + task_id=task_shot_01.id, + revision=1, + duration=15 + ) + preview_01 = self.generate_fixture_preview_file( + task_id=task_shot_01.id, + revision=2, + duration=25 + ) + preview_01 = self.generate_fixture_preview_file( + task_id=task_shot_01.id, + revision=3, + duration=30 + ) + preview_02 = self.generate_fixture_preview_file( + task_id=task_shot_02.id, + revision=1, + duration=20 + ) + preview_03 = self.generate_fixture_preview_file( + task_id=task_shot_03.id, + revision=1, + duration=10 + ) + preview_e201 = self.generate_fixture_preview_file( + task_id=task_shot_e201.id, + revision=1, + duration=40 + ) + return ( + episode_01, + episode_02, + sequence_01, + sequence_02, + shot_01, + shot_02, + shot_03, + shot_e201, + task_shot_01, + task_shot_02, + task_shot_03, + task_shot_e201, + preview_01, + preview_02, + preview_03, + preview_e201, + ) + def assign_task(self, task_id, user_id): return tasks_service.assign_task(task_id, user_id) diff --git a/tests/services/test_shots_service.py b/tests/services/test_shots_service.py index c6f39cc09e..ffc3d62e17 100644 --- a/tests/services/test_shots_service.py +++ b/tests/services/test_shots_service.py @@ -246,3 +246,63 @@ def test_get_scenes_for_sequence(self): ) scenes = shots_service.get_scenes_for_sequence(self.sequence.id) self.assertEqual(len(scenes), 1) + + def test_set_frames_from_task_type_previews(self): + self.generate_fixture_department() + self.generate_fixture_task_status() + self.generate_fixture_task_type() + self.generate_fixture_person() + self.generate_fixture_assigner() + project_id = str(self.project.id) + task_type = self.task_type_animation + task_type_id = str(task_type.id) + print(task_type_id) + ( + episode_01, + episode_02, + sequence_01, + sequence_02, + shot_01, + shot_02, + shot_03, + shot_e201, + task_shot_01, + task_shot_02, + task_shot_03, + task_shot_e201, + preview_01, + preview_02, + preview_03, + preview_e201, + ) = self.generate_fixture_shot_tasks_and_previews( + task_type_id + ) + + shots_service.set_frames_from_task_type_preview_files( + project_id, + task_type_id, + episode_id=episode_01.id + ) + + self.assertEqual( + 3, len(shots_service.get_shots_for_episode(episode_01.id)) + ) + self.assertEqual( + 1, len(shots_service.get_shots_for_episode(episode_02.id)) + ) + + shot_01 = shots_service.get_shot(shot_01.id) + shot_02 = shots_service.get_shot(shot_02.id) + shot_03 = shots_service.get_shot(shot_03.id) + shot_e201 = shots_service.get_shot(shot_e201.id) + self.assertEqual(shot_01["nb_frames"], 750) + self.assertEqual(shot_02["nb_frames"], 500) + self.assertEqual(shot_03["nb_frames"], 250) + self.assertEqual(shot_e201["nb_frames"], 0) + + shots_service.set_frames_from_task_type_preview_files( + project_id, + task_type_id + ) + shot_e201 = shots_service.get_shot(shot_e201["id"]) + self.assertEqual(shot_e201["nb_frames"], 1000) \ No newline at end of file diff --git a/tests/shots/test_shots.py b/tests/shots/test_shots.py index 0131739d5b..733d79a3cf 100644 --- a/tests/shots/test_shots.py +++ b/tests/shots/test_shots.py @@ -18,13 +18,15 @@ def setUp(self): self.shot_dict["sequence_name"] = self.sequence.name self.serialized_shot = self.shot.serialize(obj_type="Shot") self.shot_id = str(self.shot.id) + self.shot_01 = self.shot self.serialized_sequence = self.sequence.serialize(obj_type="Sequence") self.serialized_episode = self.episode.serialize(obj_type="Episode") self.serialized_project = self.project.serialize() shot_02 = self.generate_fixture_shot("SH02") + self.shot_02 = shot_02 self.shot_02_id = str(shot_02.id) - self.generate_fixture_shot("SH03") + self.shot_03 = self.generate_fixture_shot("SH03") self.generate_fixture_asset() self.generate_fixture_project_standard() @@ -148,3 +150,83 @@ def test_get_shots_by_project_and_name(self): shots = self.get( "data/shots/all?project_id=%s&name=SH01" % self.project_id, 403 ) + + def test_set_frames_from_task_type_previews(self): + self.generate_fixture_department() + self.generate_fixture_task_status() + self.generate_fixture_task_type() + self.generate_fixture_person() + self.generate_fixture_assigner() + project_id = str(self.project.id) + task_type = self.task_type_animation + task_type_id = str(task_type.id) + self.shot = self.shot_01 + self.shot_02.delete() + self.shot_03.delete() + + ( + episode_01, + episode_02, + sequence_01, + sequence_02, + shot_01, + shot_02, + shot_03, + shot_e201, + task_shot_01, + task_shot_02, + task_shot_03, + task_shot_e201, + preview_01, + preview_02, + preview_03, + preview_e201, + ) = self.generate_fixture_shot_tasks_and_previews(task_type_id) + + + self.post( + "actions/projects/%s/task-types/%s/set-shot-nb-frames?episode_id=%s" % ( + "wrong-id", + task_type_id, + str(episode_01.id) + ), {}, 400 + ) + self.post( + "actions/projects/%s/task-types/%s/set-shot-nb-frames?episode_id=%s" % ( + project_id, + "wrong-id", + str(episode_01.id) + ), {}, 400 + ) + + self.post( + "actions/projects/%s/task-types/%s/set-shot-nb-frames?episode_id=%s" % ( + project_id, + task_type_id, + "wrong-id" + ), {}, 400 + ) + self.post( + "actions/projects/%s/task-types/%s/set-shot-nb-frames?episode_id=%s" % ( + project_id, + task_type_id, + str(episode_01.id) + ), {}, 200 + ) + shot_01 = shots_service.get_shot(shot_01.id) + shot_02 = shots_service.get_shot(shot_02.id) + shot_03 = shots_service.get_shot(shot_03.id) + shot_e201 = shots_service.get_shot(shot_e201.id) + self.assertEqual(shot_01["nb_frames"], 750) + self.assertEqual(shot_02["nb_frames"], 500) + self.assertEqual(shot_03["nb_frames"], 250) + self.assertEqual(shot_e201["nb_frames"], 0) + + self.post( + "actions/projects/%s/task-types/%s/set-shot-nb-frames?episode_id=" % ( + project_id, + task_type_id, + ), {}, 200 + ) + shot_e201 = shots_service.get_shot(shot_e201["id"]) + self.assertEqual(shot_e201["nb_frames"], 1000) \ No newline at end of file diff --git a/zou/app/blueprints/shots/__init__.py b/zou/app/blueprints/shots/__init__.py index 45fa42ab3e..27d348f158 100644 --- a/zou/app/blueprints/shots/__init__.py +++ b/zou/app/blueprints/shots/__init__.py @@ -42,6 +42,7 @@ EpisodeAssetTasksResource, SequenceShotTasksResource, ProjectQuotasResource, + SetShotsFramesResource, ) routes = [ @@ -94,6 +95,10 @@ "/data/projects//quotas/", ProjectQuotasResource, ), + ( + "/actions/projects//task-types//set-shot-nb-frames", + SetShotsFramesResource, + ) ] diff --git a/zou/app/blueprints/shots/resources.py b/zou/app/blueprints/shots/resources.py index b3a4141fdf..73e2ba185d 100644 --- a/zou/app/blueprints/shots/resources.py +++ b/zou/app/blueprints/shots/resources.py @@ -18,6 +18,7 @@ from zou.app.mixin import ArgsMixin from zou.app.utils import fields, query, permissions +from zou.app.services.exception import WrongParameterException class ShotResource(Resource, ArgsMixin): @@ -1526,3 +1527,51 @@ def get(self, project_id, task_type_id): return shots_service.get_raw_quotas( project_id, task_type_id, args["studio_id"] ) + + + +class SetShotsFramesResource(Resource, ArgsMixin): + @jwt_required() + def post(self, project_id, task_type_id): + """ + Set frames for given shots. + --- + tags: + - Shots + parameters: + - in: formData + name: shots + required: True + type: array + items: + type: object + properties: + shot_id: + type: string + format: UUID + x-example: a24a6ea4-ce75-4665-a070-57453082c25 + nb_frames: + type: integer + x-example: 24 + responses: + 200: + description: Frames set for given shots + """ + user_service.check_manager_project_access(project_id) + if not fields.is_valid_id(task_type_id) or \ + not fields.is_valid_id(project_id): + raise WrongParameterException("Invalid project or task type id") + + episode_id = self.get_episode_id() + if not episode_id in ["", None] and \ + not fields.is_valid_id(episode_id): + raise WrongParameterException("Invalid episode id") + + if episode_id == "": + episode_id = None + + return shots_service.set_frames_from_task_type_preview_files( + project_id, + task_type_id, + episode_id=episode_id, + ) diff --git a/zou/app/services/shots_service.py b/zou/app/services/shots_service.py index f5b03e50b3..1269adaa85 100644 --- a/zou/app/services/shots_service.py +++ b/zou/app/services/shots_service.py @@ -20,6 +20,7 @@ ) from zou.app.models.person import Person from zou.app.models.project import Project +from zou.app.models.preview_file import PreviewFile from zou.app.models.schedule_item import ScheduleItem from zou.app.models.subscription import Subscription from zou.app.models.task import Task @@ -1525,3 +1526,75 @@ def get_all_raw_shots(): """ query = Entity.query.filter(Entity.entity_type_id == get_shot_type()["id"]) return query.all() + + +def set_frames_from_task_type_preview_files( + project_id, + task_type_id, + episode_id=None, +): + from zou.app import db + + shot_type = get_shot_type() + Shot = aliased(Entity) + Sequence = aliased(Entity) + + if episode_id is not None: + subquery = ( + db.session.query( + Shot.id.label("entity_id"), + func.max(PreviewFile.created_at).label("max_created_at") + ) + .join(Task, PreviewFile.task_id == Task.id) + .join(Shot, Task.entity_id == Shot.id) + .join(Sequence, Sequence.id == Shot.parent_id) + .filter(Shot.project_id == project_id) + .filter(Shot.entity_type_id == shot_type["id"]) + .filter(Task.task_type_id == task_type_id) + .filter(Sequence.parent_id == episode_id) + .group_by(Shot.id) + .subquery() + ) + else: + subquery = ( + db.session.query( + Shot.id.label("entity_id"), + func.max(PreviewFile.created_at).label("max_created_at") + ) + .join(Task, PreviewFile.task_id == Task.id) + .join(Shot, Task.entity_id == Shot.id) + .filter(Shot.project_id == project_id) + .filter(Shot.entity_type_id == shot_type["id"]) + .filter(Task.task_type_id == task_type_id) + .group_by(Shot.id) + .subquery() + ) + + query = ( + db.session.query( + Shot, + PreviewFile.duration + ) + .join(Task, Task.entity_id == Shot.id) + .join(subquery, (Shot.id == subquery.c.entity_id)) + .join(PreviewFile, + (PreviewFile.task_id == Task.id) & + (PreviewFile.created_at == subquery.c.max_created_at)) + .filter(Task.task_type_id == task_type_id) + .filter(Shot.project_id == project_id) + ) + + results = query.all() + project = projects_service.get_project(project_id) + updates = [] + for (shot, preview_duration) in results: + nb_frames = round(preview_duration * int(project["fps"])) + updates.append({ + "id": shot.id, + "nb_frames": nb_frames, + }) + clear_shot_cache(str(shot.id)) + + db.session.bulk_update_mappings(Shot, updates) + db.session.commit() + return updates