From 59220f6f43a23597963b2cb1bfa464f12f25844e Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Tue, 5 Mar 2024 15:40:02 +0100 Subject: [PATCH 1/3] [tasks] Add a route to get all tasks for open projects --- zou/app/blueprints/tasks/__init__.py | 2 + zou/app/blueprints/tasks/resources.py | 96 +++++++++++ zou/app/services/tasks_service.py | 236 +++++++++++++++++++++++++- 3 files changed, 333 insertions(+), 1 deletion(-) diff --git a/zou/app/blueprints/tasks/__init__.py b/zou/app/blueprints/tasks/__init__.py index 87597d9288..0d266b8f84 100644 --- a/zou/app/blueprints/tasks/__init__.py +++ b/zou/app/blueprints/tasks/__init__.py @@ -2,6 +2,7 @@ from zou.app.utils.api import configure_api_from_blueprint from zou.app.blueprints.tasks.resources import ( + OpenTasksResource, TaskFullResource, TaskForEntityResource, DeleteTasksResource, @@ -38,6 +39,7 @@ routes = [ + ("/data/tasks/open-tasks", OpenTasksResource), ("/data/tasks//comments", TaskCommentsResource), ("/data/tasks//comments/", TaskCommentResource), ("/data/tasks//previews", TaskPreviewsResource), diff --git a/zou/app/blueprints/tasks/resources.py b/zou/app/blueprints/tasks/resources.py index 31e5f70a80..b783225a12 100644 --- a/zou/app/blueprints/tasks/resources.py +++ b/zou/app/blueprints/tasks/resources.py @@ -1568,3 +1568,99 @@ def get(self): """ permissions.check_admin_permissions() return tasks_service.get_persons_tasks_dates() + + +class OpenTasksResource(Resource, ArgsMixin): + """ + Return all tasks related to open projects. + """ + + @jwt_required() + def get(self): + """ + Return all tasks related to open projects. + --- + tags: + - Tasks + parameters: + - in: query + name: project_id + description: Filter tasks on given project ID + type: string + format: UUID + x-example: a24a6ea4-ce75-4665-a070-57453082c25 + - in: query + name: task_status_id + description: Filter tasks on given task status ID + type: string + format: UUID + x-example: a24a6ea4-ce75-4665-a070-57453082c25 + - in: query + name: task_type_id + description: Filter tasks on given task type ID ID + type: string + format: UUID + x-example: a24a6ea4-ce75-4665-a070-57453082c25 + - in: query + name: person_id + description: Filter tasks on given person ID + type: string + format: UUID + x-example: a24a6ea4-ce75-4665-a070-57453082c25 + - in: query + name: start_date + description: Filter tasks posterior to given start date + type: string + format: date + x-example: "2022-07-12" + - in: query + name: due_date + description: Filter tasks anterior to given due date + type: string + format: date + x-example: "2022-07-12" + - in: query + name: priority + description: Filter tasks on given priority + type: integer + x-example: "3" + - in: query + name: page + description: Page number + type: integer + x-example: 1 + default: 1 + - in: query + name: limit + description: Number of tasks per page + type: integer + x-example: 100 + default: 100 + + responses: + 200: + description: All tasks related to open projects + """ + args = self.get_args([ + ("task_type_id", None, False, str), + ("project_id", None, False, str), + ("person_id", None, False, str), + ("task_status_id", None, False, str), + ("start_date", None, False, str), + ("due_date", None, False, str), + ("priority", None, False, str), + ("group_by", None, False, str), + ("page", None, False, int), + ("limit", 100, False, int), + ]) + return tasks_service.get_open_tasks( + task_type_id=args["task_type_id"], + project_id=args["project_id"], + person_id=args["person_id"], + task_status_id=args["task_status_id"], + start_date=args["start_date"], + due_date=args["due_date"], + priority=args["priority"], + page=args["page"], + limit=args["limit"] + ) diff --git a/zou/app/services/tasks_service.py b/zou/app/services/tasks_service.py index 946829529e..3112c9ca8c 100644 --- a/zou/app/services/tasks_service.py +++ b/zou/app/services/tasks_service.py @@ -906,7 +906,6 @@ def get_person_tasks(person_id, projects, is_done=None): cast_in_episode_names[asset_id] = [] cast_in_episode_ids[asset_id].append(episode_id) cast_in_episode_names[asset_id].append(episode_name) - print(cast_in_episode_names) # Build the result @@ -1768,3 +1767,238 @@ def get_persons_tasks_dates(): } for (person_id, min_date, max_date) in query.all() ] + + +def get_open_tasks( + task_type_id=None, + task_status_id=None, + project_id=None, + person_id=None, + start_date=None, + due_date=None, + priority=None, + order_by=None, + limit=200, + page=None +): + """ + Return all tasks matching given filters from open projects. + """ + Sequence = aliased(Entity, name="sequence") + Episode = aliased(Entity, name="episode") + + from zou.app import db + query_stats = ( + db.session.query( + func.count().label("amount"), + func.sum(Task.duration).label("total_duration"), + func.sum(Task.estimation).label("total_estimation"), + ) + .join(TaskType, Task.task_type_id == TaskType.id) + .join(TaskStatus, Task.task_status_id == TaskStatus.id) + .join(Entity, Entity.id == Task.entity_id) + .join(EntityType, EntityType.id == Entity.entity_type_id) + .join(Project, Project.id == Task.project_id) + .join(ProjectStatus, ProjectStatus.id == Project.project_status_id) + .outerjoin(Sequence, Sequence.id == Entity.parent_id) + .outerjoin(Episode, Episode.id == Sequence.parent_id) + ) + query = ( + Task.query + .join(TaskType, Task.task_type_id == TaskType.id) + .join(TaskStatus, Task.task_status_id == TaskStatus.id) + .join(Entity, Entity.id == Task.entity_id) + .join(EntityType, EntityType.id == Entity.entity_type_id) + .join(Project, Project.id == Task.project_id) + .join(ProjectStatus, ProjectStatus.id == Project.project_status_id) + .outerjoin(Sequence, Sequence.id == Entity.parent_id) + .outerjoin(Episode, Episode.id == Sequence.parent_id) + .add_columns( + Project.name, + Project.has_avatar, + Entity.id, + Entity.name, + Entity.description, + Entity.data, + Entity.preview_file_id, + EntityType.name, + Entity.canceled, + Entity.parent_id, + Entity.source_id, + Sequence.name, + Episode.id, + Episode.name, + TaskType.name, + TaskType.for_entity, + TaskStatus.name, + TaskType.color, + TaskStatus.color, + TaskStatus.short_name, + ) + ).order_by( + Project.name, + Episode.name, + Sequence.name, + EntityType.name, + Entity.name, + TaskType.name, + ) + + if project_id is not None and user_service.check_project_access(project_id): + query = query.filter(Project.id == project_id) + query_stats = query_stats.filter(Project.id == project_id) + else: + from zou.app.utils import permissions + if permissions.has_admin_permissions(): + query = query.filter(ProjectStatus.name == "Open") + query_stats = query_stats.filter(ProjectStatus.name == "Open") + else: + query = query.filter(user_service.build_related_projects_filter()) + query_stats = query_stats.filter(user_service.build_related_projects_filter()) + + if task_type_id is not None: + query = query.filter(TaskType.id == task_type_id) + query_stats = query_stats.filter(TaskType.id == task_type_id) + + if task_status_id is not None: + query = query.filter(TaskStatus.id == task_status_id) + query_stats = query_stats.filter(TaskStatus.id == task_status_id) + + if person_id is not None: + query = query.filter(Task.assignees.any(id=person_id)) + query_stats = query_stats.filter(Task.assignees.any(id=person_id)) + + if start_date is not None: + query = query.filter(Task.start_date >= start_date) + query_stats = query_stats.filter(Task.start_date >= start_date) + + if due_date is not None: + query = query.filter(Task.due_date <= due_date) + query_stats = query_stats.filter(Task.due_date <= due_date) + + if priority is not None: + query = query.filter(TaskType.priority == priority) + query_stats = query_stats.filter(TaskType.priority == priority) + + if page is not None and int(page) > 0: + query = query.offset((page - 1) * limit) + + if order_by is not None: + query = query.order_by(order_by) + + query_stats_status = ( + query_stats + .group_by(TaskStatus.id) + .add_columns(TaskStatus.id) + ) + + tasks = [] + + for ( + task, + project_name, + project_has_avatar, + entity_id, + entity_name, + entity_description, + entity_data, + entity_preview_file_id, + entity_type_name, + entity_canceled, + entity_parent_id, + entity_source_id, + sequence_name, + episode_id, + episode_name, + task_type_name, + task_type_for_entity, + task_status_name, + task_type_color, + task_status_color, + task_status_short_name, + ) in query.limit(limit).all(): + if entity_preview_file_id is None: + entity_preview_file_id = "" + + if entity_source_id is None: + entity_source_id = "" + + if episode_id is None: + episode_id = entity_source_id + if episode_id is not None and episode_id != "": + try: + episode = shots_service.get_episode(episode_id) + episode_name = episode["name"] + except EpisodeNotFoundException: + episode_name = "MP" + + task_dict = get_task_with_relations(str(task.id)) + if entity_type_name == "Sequence" and entity_parent_id is not None: + episode_id = entity_parent_id + episode = shots_service.get_episode(episode_id) + episode_name = episode["name"] + + task_dict.update({ + "project_name": project_name, + "project_id": str(task.project_id), + "project_has_avatar": project_has_avatar, + "entity_id": str(entity_id), + "entity_name": entity_name, + "entity_description": entity_description, + "entity_data": entity_data, + "entity_preview_file_id": str(entity_preview_file_id), + "entity_source_id": str(entity_source_id), + "entity_type_name": entity_type_name, + "entity_canceled": entity_canceled, + "sequence_name": sequence_name, + "episode_id": str(episode_id), + "episode_name": episode_name, + "estimation": task.estimation, + "duration": task.duration, + "start_date": fields.serialize_value(task.start_date), + "due_date": fields.serialize_value(task.due_date), + "type_name": task_type_name, + "task_type_for_entity": task_type_for_entity, + "status_name": task_status_name, + "type_color": task_type_color, + "status_color": task_status_color, + "status_short_name": task_status_short_name, + }) + tasks.append(task_dict) + + result = { + "data": [], + "stats": { + "total_duration": 0, + "total_estimation": 0, + "total": 0, + "status": [] + }, + "limit": limit, + "is_more": False, + "page": page or 1 + } + + if len(tasks) > 0: + count = query.count() + stats = query_stats.one() + stats_status = query_stats_status.all() + statuses_stats = [ + { "task_status_id": stat.id, "amount": stat.amount } + for stat in stats_status + ] + + result = { + "data": tasks, + "total": count, + "stats": { + "total_duration": stats.total_duration, + "total_estimation": stats.total_estimation, + "total": count, + "status": statuses_stats + }, + "limit": limit, + "is_more": len(tasks) == limit, + "page": page or 1 + } + return result From 235bfa46104366b2ccaa16c390d58b5555901f01 Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Wed, 6 Mar 2024 18:57:18 +0100 Subject: [PATCH 2/3] [qa] Add tests and documentation to the new open tasks route --- tests/tasks/test_route_tasks.py | 60 +++++++++++++++++++++++++++ zou/app/blueprints/tasks/resources.py | 31 +++++++++++++- zou/app/services/tasks_service.py | 1 - 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/tests/tasks/test_route_tasks.py b/tests/tasks/test_route_tasks.py index 4323f17ea5..a26ee8f18b 100644 --- a/tests/tasks/test_route_tasks.py +++ b/tests/tasks/test_route_tasks.py @@ -535,3 +535,63 @@ def test_update_entity_main_preview_from_task(self): ) entity = self.get("/data/entities/%s" % task["entity_id"]) self.assertEqual(entity["preview_file_id"], preview_file["id"]) + + def test_open_tasks(self): + self.generate_fixture_task() + task_id = str(self.task.id) + self.task.update({"task_status_id": self.wip_status_id}) + self.generate_fixture_asset_types() + self.generate_fixture_asset_character() + self.generate_fixture_task( + entity_id=self.asset_character.id + ) + + animation_id = str(self.task_type_animation.id) + self.generate_fixture_shot_task() + self.generate_fixture_shot("SHOT_002") + self.generate_fixture_shot_task() + self.generate_fixture_shot("SHOT_003") + self.generate_fixture_shot_task() + + tasks = self.get("/data/tasks/open-tasks") + self.assertEqual(len(tasks["data"]), 5) + + self.generate_fixture_project_closed_status() + self.generate_fixture_project_closed() + asset = self.generate_fixture_asset( + project_id=self.project_closed.id + ) + self.generate_fixture_task( + entity_id=asset.id, + project_id=self.project_closed.id + ) + tasks = self.get("/data/tasks/open-tasks") + self.assertEqual(len(tasks["data"]), 5) + + self.generate_fixture_project_standard() + self.generate_fixture_asset_standard() + self.generate_fixture_task_standard() + tasks = self.get("/data/tasks/open-tasks") + self.assertEqual(len(tasks["data"]), 6) + + tasks = self.get("/data/tasks/open-tasks?project_id=%s" % self.project.id) + self.assertEqual(len(tasks["data"]), 5) + + tasks = self.get("/data/tasks/open-tasks?project_id=%s&limit=3" % self.project.id) + self.assertEqual(len(tasks["data"]), 3) + + tasks = self.get("/data/tasks/open-tasks?project_id=%s&limit=3&page=2" % self.project.id) + self.assertEqual(len(tasks["data"]), 2) + + tasks = self.get("/data/tasks/open-tasks?task_type_id=%s" % animation_id) + self.assertEqual(len(tasks["data"]), 3) + + tasks = self.get("/data/tasks/open-tasks?task_status_id=%s" % self.wip_status_id) + self.assertEqual(len(tasks["data"]), 1) + + jane = self.generate_fixture_person("Jane", "Doe", "jane.doe", "jane.doe@gmail.com") + data = {"person_id": jane.id} + self.put("/actions/tasks/%s/assign" % task_id, data, 200) + self.put("/actions/tasks/%s/assign" % self.shot_task.id, data, 200) + tasks = self.get("/data/tasks/open-tasks?person_id=%s" % jane.id) + self.assertEqual(len(tasks["data"]), 2) diff --git a/zou/app/blueprints/tasks/resources.py b/zou/app/blueprints/tasks/resources.py index b783225a12..36d8bd6b2a 100644 --- a/zou/app/blueprints/tasks/resources.py +++ b/zou/app/blueprints/tasks/resources.py @@ -1639,7 +1639,36 @@ def get(self): responses: 200: - description: All tasks related to open projects + schema: + type: object + properties: + data: + type: array + description: List of tasks + stats: + type: object + properties: + total: + type: integer + description: Total number of tasks + total_duration: + type: integer + description: Total duration of tasks in minutes + total_estimation: + type: integer + description: Total estimation of tasks in minutes + status: + type: object + description: Number of tasks per status + limit: + type: integer + description: Number of tasks per page + page: + type: integer + description: Page number + is_more: + type: boolean + description: True if there are more tasks to retrieve """ args = self.get_args([ ("task_type_id", None, False, str), diff --git a/zou/app/services/tasks_service.py b/zou/app/services/tasks_service.py index 3112c9ca8c..50198f04ba 100644 --- a/zou/app/services/tasks_service.py +++ b/zou/app/services/tasks_service.py @@ -1990,7 +1990,6 @@ def get_open_tasks( result = { "data": tasks, - "total": count, "stats": { "total_duration": stats.total_duration, "total_estimation": stats.total_estimation, From 029fd2f4d60995e103804283307123f71966bad3 Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 7 Mar 2024 15:19:01 +0100 Subject: [PATCH 3/3] [qa] Allow more custumosiation in fixture generation --- tests/base.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/base.py b/tests/base.py index 95aba5843c..c556eefc54 100644 --- a/tests/base.py +++ b/tests/base.py @@ -290,15 +290,21 @@ def generate_fixture_project_no_preview_tree(self): ) def generate_fixture_asset( - self, name="Tree", description="Description Tree", asset_type_id=None + self, name="Tree", + description="Description Tree", + asset_type_id=None, + project_id=None, ): if asset_type_id is None: asset_type_id = self.asset_type.id + if project_id is None: + project_id = self.project_id + self.asset = Entity.create( name=name, description=description, - project_id=self.project.id, + project_id=project_id, entity_type_id=asset_type_id, ) return self.asset @@ -500,7 +506,7 @@ def generate_fixture_user_vendor(self): def generate_fixture_person( self, first_name="John", - last_name="Doe", + last_name="Doe", desktop_login="john.doe", email="john.doe@gmail.com", ): @@ -655,7 +661,7 @@ def generate_fixture_assigner(self): return self.assigner def generate_fixture_task( - self, name="Master", entity_id=None, task_type_id=None + self, name="Master", entity_id=None, task_type_id=None, project_id=None ): if entity_id is None: entity_id = self.asset.id @@ -663,12 +669,15 @@ def generate_fixture_task( if task_type_id is None: task_type_id = self.task_type.id + if project_id is None: + project_id = self.project.id + start_date = fields.get_date_object("2017-02-20") due_date = fields.get_date_object("2017-02-28") real_start_date = fields.get_date_object("2017-02-22") self.task = Task.create( name=name, - project_id=self.project.id, + project_id=project_id, task_type_id=task_type_id, task_status_id=self.task_status.id, entity_id=entity_id,