Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions tests/projects/test_route_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,33 @@ def test_all_projects_metadata_descriptor(self):
any(d["field_name"] == "ship_code" for d in descriptors)
)

def test_new_project_copies_project_descriptors(self):
# A Project descriptor on an open project and one on a closed
# project: only the open one is copied onto a new project.
self.post(
f"data/projects/{self.project_id}/metadata-descriptors",
{
"entity_type": "Project",
"name": "Delivery code",
"data_type": "string",
},
)
projects_service.add_metadata_descriptor(
str(self.project_closed.id),
"Project",
"Closed only",
"string",
[],
False,
)
project = self.post("data/projects", {"name": "Fresh Project"}, 201)
descriptors = self.get(
f"data/projects/{project['id']}/metadata-descriptors"
)
field_names = [d["field_name"] for d in descriptors]
self.assertIn("delivery_code", field_names)
self.assertNotIn("closed_only", field_names)

def test_add_asset_metadata_descriptor(self):
descriptor = self.post(
f"data/projects/{self.project_id}/metadata-descriptors",
Expand Down
122 changes: 122 additions & 0 deletions tests/projects/test_route_project_settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from tests.base import ApiDBTestCase

from zou.app.models.project import ProjectTaskTypeLink
from zou.app.services import budget_service
from zou.app.utils import fields


class ProjectSettingsRoutesTestCase(ApiDBTestCase):
Expand Down Expand Up @@ -97,6 +99,126 @@ def test_delete_project_task_status(self):
status_ids = [s["id"] for s in statuses]
self.assertNotIn(str(self.task_status.id), status_ids)

# --- Batch settings ---

def test_add_project_settings_batch(self):
result = self.post(
f"/data/projects/{self.project_id}/settings/batch",
{
"task_types": [
{"task_type_id": str(self.task_type.id), "priority": 1},
{
"task_type_id": str(self.task_type_modeling.id),
"priority": 2,
},
],
"task_status_ids": [str(self.task_status.id)],
"asset_type_ids": [str(self.asset_type.id)],
},
200,
)
self.assertIsNotNone(result.get("id"))
project = self.get(f"/data/projects/{self.project_id}")
self.assertIn(str(self.task_type.id), project["task_types"])
self.assertIn(str(self.task_type_modeling.id), project["task_types"])
self.assertIn(str(self.asset_type.id), project["asset_types"])
statuses = self.get(
f"/data/projects/{self.project_id}/settings/task-status"
)
self.assertIn(str(self.task_status.id), [s["id"] for s in statuses])
link = ProjectTaskTypeLink.get_by(
project_id=self.project_id, task_type_id=str(self.task_type.id)
)
self.assertEqual(link.priority, 1)

def test_project_settings_batch_is_idempotent(self):
data = {
"task_types": [{"task_type_id": str(self.task_type.id)}],
"task_status_ids": [
str(self.task_status.id),
str(self.task_status.id),
],
"asset_type_ids": [str(self.asset_type.id)],
}
self.post(
f"/data/projects/{self.project_id}/settings/batch", data, 200
)
self.post(
f"/data/projects/{self.project_id}/settings/batch", data, 200
)
project = self.get(f"/data/projects/{self.project_id}")
self.assertEqual(
project["task_types"].count(str(self.task_type.id)), 1
)
self.assertEqual(
project["asset_types"].count(str(self.asset_type.id)), 1
)
statuses = self.get(
f"/data/projects/{self.project_id}/settings/task-status"
)
status_ids = [s["id"] for s in statuses]
self.assertEqual(status_ids.count(str(self.task_status.id)), 1)

def test_project_settings_batch_replace_task_types(self):
self.post(
f"/data/projects/{self.project_id}/settings/batch",
{
"task_types": [
{"task_type_id": str(self.task_type.id), "priority": 1},
{
"task_type_id": str(self.task_type_concept.id),
"priority": 2,
},
]
},
200,
)
self.post(
f"/data/projects/{self.project_id}/settings/batch",
{
"task_types": [
{"task_type_id": str(self.task_type.id), "priority": 2},
{
"task_type_id": str(self.task_type_modeling.id),
"priority": 1,
},
],
"replace_task_types": True,
},
200,
)
project = self.get(f"/data/projects/{self.project_id}")
self.assertIn(str(self.task_type.id), project["task_types"])
self.assertIn(str(self.task_type_modeling.id), project["task_types"])
self.assertNotIn(str(self.task_type_concept.id), project["task_types"])
link = ProjectTaskTypeLink.get_by(
project_id=self.project_id, task_type_id=str(self.task_type.id)
)
self.assertEqual(link.priority, 2)

def test_project_settings_batch_skips_unknown_ids(self):
self.post(
f"/data/projects/{self.project_id}/settings/batch",
{
"task_types": [{"task_type_id": fields.gen_uuid()}],
"task_status_ids": [fields.gen_uuid()],
"asset_type_ids": [fields.gen_uuid()],
},
200,
)
project = self.get(f"/data/projects/{self.project_id}")
self.assertEqual(project["task_types"], [])
self.assertEqual(project["asset_types"], [])

def test_project_settings_batch_as_artist_is_forbidden(self):
self.generate_fixture_user_cg_artist()
self.log_in_cg_artist()
self.post(
f"/data/projects/{self.project_id}/settings/batch",
{"task_types": [{"task_type_id": str(self.task_type.id)}]},
403,
)

# --- Status automations settings ---

def test_get_project_status_automations(self):
Expand Down
19 changes: 19 additions & 0 deletions tests/tasks/test_route_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,25 @@ def test_clear_assignation(self):
task = tasks_service.get_task(shot_task_id, relations=True)
self.assertEqual(len(task["assignees"]), 0)

def test_update_task_assignees(self):
# The fixture task starts assigned to self.person.
self.generate_fixture_task()
task_id = str(self.task.id)
person_id = str(self.person.id)
self.put(f"data/tasks/{task_id}", {"assignees": []}, 200)
task = tasks_service.get_task(task_id, relations=True)
self.assertEqual(task["assignees"], [])
self.put(f"data/tasks/{task_id}", {"assignees": [person_id]}, 200)
task = tasks_service.get_task(task_id, relations=True)
self.assertEqual(task["assignees"], [person_id])
# The generic update emits the same task:assign event as the assign
# route, so the assignation notification is created too.
notifications = notifications_service.get_last_notifications(
"assignation"
)
self.assertEqual(len(notifications), 1)
self.assertEqual(str(notifications[0]["person_id"]), person_id)

def test_set_tasks_priority(self):
self.generate_fixture_task()
self.generate_fixture_shot_task()
Expand Down
5 changes: 5 additions & 0 deletions zou/app/blueprints/crud/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@ def post_creation(self, project):
project_dict["first_episode_id"] = fields.serialize_value(
episode["id"]
)
# The all-projects metadata columns are one Project descriptor row
# per project: copy them onto the new project so its cells are
# editable right away, instead of one create request per descriptor
# from the client.
projects_service.copy_project_metadata_descriptors(str(project.id))
user_service.clear_project_cache()
projects_service.clear_project_cache("")
return project_dict
Expand Down
36 changes: 35 additions & 1 deletion zou/app/blueprints/crud/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
deletion_service,
entities_service,
assets_service,
notifications_service,
persons_service,
)
from zou.app.utils import permissions
from zou.app.utils import events, permissions

from zou.app.services.exception import WrongTaskTypeForEntityException

Expand Down Expand Up @@ -440,8 +442,40 @@ def check_update_permissions(self, task, data):
def check_delete_permissions(self, task):
user_service.check_manager_project_access(task["project_id"])

def pre_update(self, instance_dict, data):
if "assignees" in data:
self._previous_assignees = {
str(person.id) for person in self.instance.assignees
}
return instance_dict

def post_update(self, instance_dict, data):
tasks_service.clear_task_cache(instance_dict["id"])
# Assignees set through the generic update emit the same events and
# assignation notifications as the assign/clear-assignation routes
# so that listeners and assignees stay in sync.
if "assignees" in data:
new_assignees = {
str(person.id) for person in self.instance.assignees
}
previous_assignees = getattr(self, "_previous_assignees", set())
project_id = instance_dict["project_id"]
current_user_id = persons_service.get_current_user()["id"]
for person_id in new_assignees - previous_assignees:
events.emit(
"task:assign",
{"task_id": instance_dict["id"], "person_id": person_id},
project_id=project_id,
)
notifications_service.create_assignation_notification(
instance_dict["id"], person_id, current_user_id
)
for person_id in previous_assignees - new_assignees:
events.emit(
"task:unassign",
{"task_id": instance_dict["id"], "person_id": person_id},
project_id=project_id,
)
return instance_dict

@jwt_required()
Expand Down
5 changes: 5 additions & 0 deletions zou/app/blueprints/projects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ProductionTaskTypesResource,
ProductionTaskStatusResource,
ProductionTaskStatusRemoveResource,
ProductionSettingsBatchResource,
ProductionStatusAutomationResource,
ProductionStatusAutomationRemoveResource,
ProductionMetadataDescriptorResource,
Expand Down Expand Up @@ -84,6 +85,10 @@
"/data/projects/<project_id>/settings/task-status/<task_status_id>",
ProductionTaskStatusRemoveResource,
),
(
"/data/projects/<project_id>/settings/batch",
ProductionSettingsBatchResource,
),
(
"/data/projects/<project_id>/settings/status-automations",
ProductionStatusAutomationResource,
Expand Down
73 changes: 73 additions & 0 deletions zou/app/blueprints/projects/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ProjectAssetTypeSchema,
ProjectTaskTypeSchema,
ProjectTaskStatusSchema,
ProjectSettingsBatchSchema,
ProjectStatusAutomationSchema,
ProjectPreviewBackgroundSchema,
MetadataDescriptorSchema,
Expand Down Expand Up @@ -693,6 +694,78 @@ def delete(self, project_id, task_status_id):
return "", 204


class ProductionSettingsBatchResource(MethodView):
"""
Allow to add several task types, task statuses and asset types to a
production in a single request.
"""

@jwt_required()
def post(self, project_id):
"""
Add settings to production batch
---
description: Add several task types (with their priority), task
statuses and asset types to a production in a single request,
replacing one link request per item. Unknown ids are skipped.
When replace_task_types is set, the task type list is the full
wanted set and existing links absent from it are removed.
tags:
- Projects
parameters:
- in: path
name: project_id
required: true
schema:
type: string
format: uuid
description: Project unique identifier
example: a24a6ea4-ce75-4665-a070-57453082c25
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
task_types:
type: array
items:
type: object
properties:
task_type_id:
type: string
format: uuid
priority:
type: integer
task_status_ids:
type: array
items:
type: string
format: uuid
asset_type_ids:
type: array
items:
type: string
format: uuid
replace_task_types:
type: boolean
default: false
responses:
200:
description: Updated project
"""
body = validation.validate_request_body(ProjectSettingsBatchSchema)
user_service.check_manager_project_access(project_id)
return projects_service.update_project_settings(
project_id,
task_types=[entry.model_dump() for entry in body.task_types],
task_status_ids=body.task_status_ids,
asset_type_ids=body.asset_type_ids,
replace_task_types=body.replace_task_types,
)


class ProductionStatusAutomationResource(MethodView, ArgsMixin):
"""
Allow to add a status automation linked to a production.
Expand Down
14 changes: 14 additions & 0 deletions zou/app/blueprints/projects/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ class ProjectTaskStatusSchema(BaseSchema):
task_status_id: str = Field(..., min_length=1)


class ProjectSettingsBatchSchema(BaseSchema):
"""
Body for adding several task types, task statuses and asset types to a
project in a single request.
"""

task_types: List[ProjectTaskTypeSchema] = Field(default=[])
task_status_ids: List[str] = Field(default=[])
asset_type_ids: List[str] = Field(default=[])
# When set, task_types is the full wanted set: existing task type links
# absent from it are removed (used by the import-from-production flow).
replace_task_types: bool = False


class ProjectStatusAutomationSchema(BaseSchema):
"""
Body for adding a status automation to a project.
Expand Down
Loading
Loading