diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 786e7170..56bb59fb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,8 +5,9 @@ Stalker Changes 1.0.0 ===== -* `Version.take_name` has been renamed to `Version.variant_name` to follow the industry - standard (and then removed it completely as we now have `Variant` class for this). +* `Version.take_name` has been renamed to `Version.variant_name` to follow the + industry standard (and then removed it completely as we now have `Variant` + class for this). * `Task.depends` renamed to `Task.depends_on`. * `TaskDependency.task_depends_to` renamed to `TaskDependency.task_depends_on`. * Modernised Stalker as a Python project. It is now fully PEP 517 compliant. @@ -14,10 +15,10 @@ Stalker Changes * Stalker is now SQLAlchemy 2.x compliant. * Stalker is now fully type hinted. * Added GitHub actions for CI/CD practices. -* Updated validation messages to make them more consistently displaying the current - type and the value of the validated attribute. -* Added Makefile workflow to help creating a virtualenv, building, installing, releasing - etc. actions much more easier. +* Updated validation messages to make them more consistently displaying the + current type and the value of the validated attribute. +* Added Makefile workflow to help creating a virtualenv, building, installing, + releasing etc. actions much more easier. * Added `tox` config to run the test with Python 3.8 to 3.13. * Increased test coverage to 99.71%. * Updated documentation theme to `furo`. @@ -25,13 +26,22 @@ Stalker Changes * `Scene` is now deriving from `Task`. * `Shot.sequences` is now `Shot.sequences` and it is many-to-one. * `Shot.scenes` is now `Shot.scene` and it is many-to-one. -* Added the `Variant` class to allow variants to be approved and managed individually. -* Added `Review.version` attribute to relate a `Version` instance to the review. -* Removed the `Version.variant_name` attribute. The migration alembic script will create - `Variant` instances for each `Version.variant_name` under the container `Task` to hold - the information. -* `Version._template_variables()` now finds the related `Asset`, `Shot` and `Sequence` - values and passes them in the returned dictionary. +* Added the `Variant` class to allow variants to be approved and managed + individually. +* Added `Review.version` attribute to relate a `Version` instance to the + review. +* Removed the `Version.variant_name` attribute. The migration alembic script + will create `Variant` instances for each `Version.variant_name` under the + container `Task` to hold the information. +* `Version._template_variables()` now finds the related `Asset`, `Shot` and + `Sequence` values and passes them in the returned dictionary. +* All the enum values handled with arbitrary string lists are now enum classes. + As a result we now have `ScheduleConstraint`, `TimeUnit`, `ScheduleModel`, + `DependencyTarget` enum classes which are removing the need of using fiddly + strings as enum values. +* `StatusList`s that are created for super classes can now be used with the + derived classes, i.e. a status list created specifically for Task can now be + used with Asset, Shot Sequence and Scenes and any future Task derivatives. 0.2.27 ====== diff --git a/src/stalker/db/session.py b/src/stalker/db/session.py index d406d777..8a297521 100644 --- a/src/stalker/db/session.py +++ b/src/stalker/db/session.py @@ -22,8 +22,8 @@ def save(self, data: Union[None, List[Any], "SimpleEntity"] = None) -> None: data (Union[list, stalker.models.entity.SimpleEntity]): Either a single or a list of :class:`stalker.models.entity.SimpleEntity` or derivatives. """ - if data: - if hasattr(data, "__getitem__"): + if data is not None: + if isinstance(data, list): self.add_all(data) else: self.add(data) diff --git a/src/stalker/db/setup.py b/src/stalker/db/setup.py index 878be40c..b1012e21 100644 --- a/src/stalker/db/setup.py +++ b/src/stalker/db/setup.py @@ -182,36 +182,6 @@ def init() -> None: status_codes=defaults.task_status_codes, user=admin, ) - create_entity_statuses( - entity_type="Asset", - status_names=defaults.task_status_names, - status_codes=defaults.task_status_codes, - user=admin, - ) - create_entity_statuses( - entity_type="Shot", - status_names=defaults.task_status_names, - status_codes=defaults.task_status_codes, - user=admin, - ) - create_entity_statuses( - entity_type="Sequence", - status_names=defaults.task_status_names, - status_codes=defaults.task_status_codes, - user=admin, - ) - create_entity_statuses( - entity_type="Scene", - status_names=defaults.task_status_names, - status_codes=defaults.task_status_codes, - user=admin, - ) - create_entity_statuses( - entity_type="Variant", - status_names=defaults.task_status_names, - status_codes=defaults.task_status_codes, - user=admin, - ) create_entity_statuses( entity_type="Review", status_names=defaults.review_status_names, diff --git a/src/stalker/models/enum.py b/src/stalker/models/enum.py index ab416116..24093dba 100644 --- a/src/stalker/models/enum.py +++ b/src/stalker/models/enum.py @@ -27,7 +27,9 @@ def __repr__(self) -> str: __str__ = __repr__ @classmethod - def to_constraint(cls, constraint: Union[int, str, "ScheduleConstraint"]) -> "ScheduleConstraint": + def to_constraint( + cls, constraint: Union[int, str, "ScheduleConstraint"] + ) -> "ScheduleConstraint": """Validate and return type enum from an input int or str value. Args: diff --git a/src/stalker/models/mixins.py b/src/stalker/models/mixins.py index b7d7735f..5ad1f1ce 100644 --- a/src/stalker/models/mixins.py +++ b/src/stalker/models/mixins.py @@ -431,6 +431,8 @@ def _validate_status_list( """ from stalker.models.status import StatusList + super_names = [mro.__name__ for mro in self.__class__.__mro__] + if status_list is None: # check if there is a db setup and try to get the appropriate # StatusList from the database @@ -440,8 +442,8 @@ def _validate_status_list( try: # try to get a StatusList with the target_entity_type is # matching the class name - status_list = StatusList.query.filter_by( - target_entity_type=self.__class__.__name__ + status_list = StatusList.query.filter( + StatusList.target_entity_type.in_(super_names) ).first() except (UnboundExecutionError, OperationalError): # it is not mapped just skip it @@ -469,7 +471,7 @@ def _validate_status_list( # check if the entity_type matches to the # StatusList.target_entity_type - if self.__class__.__name__ != status_list.target_entity_type: + if status_list.target_entity_type not in super_names: raise TypeError( "The given StatusLists' target_entity_type is " f"{status_list.target_entity_type}, " diff --git a/tests/db/test_db.py b/tests/db/test_db.py index 34c6bd4c..dae13805 100644 --- a/tests/db/test_db.py +++ b/tests/db/test_db.py @@ -276,40 +276,8 @@ def test_variant_status_list_initialization(setup_postgresql_db): variant_status_list = StatusList.query.filter( StatusList.target_entity_type == "Variant" ).first() - assert isinstance(variant_status_list, StatusList) - assert variant_status_list.name == "Variant Statuses" - expected_status_names = [ - "Waiting For Dependency", - "Ready To Start", - "Work In Progress", - "Pending Review", - "Has Revision", - "Dependency Has Revision", - "On Hold", - "Stopped", - "Completed", - ] - expected_status_codes = [ - "WFD", - "RTS", - "WIP", - "PREV", - "HREV", - "DREV", - "OH", - "STOP", - "CMPL", - ] - assert len(variant_status_list.statuses) == len(expected_status_names) - db_status_names = map(lambda x: x.name, variant_status_list.statuses) - db_status_codes = map(lambda x: x.code, variant_status_list.statuses) - assert sorted(expected_status_names) == sorted(db_status_names) - assert sorted(expected_status_codes) == sorted(db_status_codes) - # check if the created_by and updated_by attributes are correctly set - # to the admin - admin = get_admin_user() - assert all(status.created_by == admin for status in variant_status_list.statuses) - assert all(status.updated_by == admin for status in variant_status_list.statuses) + # we do not create a specific StatusList for Variant's anymore + assert variant_status_list is None def test_register_creates_suitable_permissions(setup_postgresql_db): @@ -496,290 +464,51 @@ def test_task_status_list_initialization(setup_postgresql_db): def test_asset_status_list_initialization(setup_postgresql_db): """Asset statuses are correctly created.""" asset_status_list = ( - StatusList.query.filter(StatusList.name == "Asset Statuses") - .filter(StatusList.target_entity_type == "Asset") - .first() + StatusList.query.filter(StatusList.target_entity_type == "Asset").first() ) - assert isinstance(asset_status_list, StatusList) - expected_status_names = [ - "Waiting For Dependency", - "Ready To Start", - "Work In Progress", - "Pending Review", - "Has Revision", - "Dependency Has Revision", - "On Hold", - "Stopped", - "Completed", - ] - expected_status_codes = [ - "WFD", - "RTS", - "WIP", - "PREV", - "HREV", - "DREV", - "OH", - "STOP", - "CMPL", - ] - assert len(asset_status_list.statuses) == len(expected_status_names) - db_status_names = map(lambda x: x.name, asset_status_list.statuses) - db_status_codes = map(lambda x: x.code, asset_status_list.statuses) - assert sorted(expected_status_names) == sorted(db_status_names) - assert sorted(expected_status_codes) == sorted(db_status_codes) + # we do not generate a specific StatusList for Assets anymore + # as Task specific StatusLists can be used. + assert asset_status_list is None def test_shot_status_list_initialization(setup_postgresql_db): """Shot statuses are correctly created.""" shot_status_list = ( - StatusList.query.filter(StatusList.name == "Shot Statuses") - .filter(StatusList.target_entity_type == "Shot") - .first() + StatusList.query.filter(StatusList.target_entity_type == "Shot").first() ) - assert isinstance(shot_status_list, StatusList) - expected_status_names = [ - "Waiting For Dependency", - "Ready To Start", - "Work In Progress", - "Pending Review", - "Has Revision", - "Dependency Has Revision", - "On Hold", - "Stopped", - "Completed", - ] - expected_status_codes = [ - "WFD", - "RTS", - "WIP", - "PREV", - "HREV", - "DREV", - "OH", - "STOP", - "CMPL", - ] - assert len(shot_status_list.statuses) == len(expected_status_names) - db_status_names = map(lambda x: x.name, shot_status_list.statuses) - db_status_codes = map(lambda x: x.code, shot_status_list.statuses) - assert sorted(expected_status_names) == sorted(db_status_names) - assert sorted(expected_status_codes) == sorted(db_status_codes) + # we do not generate a specific StatusList for Shots anymore + # as Task specific StatusLists can be used. + assert shot_status_list is None def test_sequence_status_list_initialization(setup_postgresql_db): """Sequence statuses are correctly created.""" sequence_status_list = ( - StatusList.query.filter(StatusList.name == "Sequence Statuses") - .filter(StatusList.target_entity_type == "Sequence") - .first() + StatusList.query.filter(StatusList.target_entity_type == "Sequence").first() ) - assert isinstance(sequence_status_list, StatusList) - expected_status_names = [ - "Waiting For Dependency", - "Ready To Start", - "Work In Progress", - "Pending Review", - "Has Revision", - "Dependency Has Revision", - "On Hold", - "Stopped", - "Completed", - ] - expected_status_codes = [ - "WFD", - "RTS", - "WIP", - "PREV", - "HREV", - "DREV", - "OH", - "STOP", - "CMPL", - ] - assert len(sequence_status_list.statuses) == len(expected_status_names) - db_status_names = map(lambda x: x.name, sequence_status_list.statuses) - db_status_codes = map(lambda x: x.code, sequence_status_list.statuses) - assert sorted(expected_status_names) == sorted(db_status_names) - assert sorted(expected_status_codes) == sorted(db_status_codes) - # check if the created_by and updated_by attributes are correctly set - # to admin - admin = get_admin_user() - assert all(status.created_by == admin for status in sequence_status_list.statuses) - assert all(status.updated_by == admin for status in sequence_status_list.statuses) + # we do not generate a specific StatusList for Sequences anymore + # as Task specific StatusLists can be used. + assert sequence_status_list is None def test_scene_status_list_initialization(setup_postgresql_db): """Scene statuses are correctly created.""" scene_status_list = ( - StatusList.query.filter(StatusList.name == "Scene Statuses") - .filter(StatusList.target_entity_type == "Scene") - .first() + StatusList.query.filter(StatusList.target_entity_type == "Scene").first() ) - assert isinstance(scene_status_list, StatusList) - expected_status_names = [ - "Waiting For Dependency", - "Ready To Start", - "Work In Progress", - "Pending Review", - "Has Revision", - "Dependency Has Revision", - "On Hold", - "Stopped", - "Completed", - ] - expected_status_codes = [ - "WFD", - "RTS", - "WIP", - "PREV", - "HREV", - "DREV", - "OH", - "STOP", - "CMPL", - ] - assert len(scene_status_list.statuses) == len(expected_status_names) - db_status_names = map(lambda x: x.name, scene_status_list.statuses) - db_status_codes = map(lambda x: x.code, scene_status_list.statuses) - assert sorted(expected_status_names) == sorted(db_status_names) - assert sorted(expected_status_codes) == sorted(db_status_codes) - # check if the created_by and updated_by attributes are correctly set - # to admin - admin = get_admin_user() - assert all(status.created_by == admin for status in scene_status_list.statuses) - assert all(status.updated_by == admin for status in scene_status_list.statuses) - - -def test_asset_status_list_initialization_when_there_is_an_asset_status_list( - setup_postgresql_db, -): - """Asset statuses created if a StatusList for Sequence exists.""" - asset_status_list = StatusList.query.filter( - StatusList.name == "Asset Statuses" - ).first() - assert isinstance(asset_status_list, StatusList) - expected_status_names = [ - "Waiting For Dependency", - "Ready To Start", - "Work In Progress", - "Pending Review", - "Has Revision", - "Dependency Has Revision", - "On Hold", - "Stopped", - "Completed", - ] - expected_status_codes = [ - "WFD", - "RTS", - "WIP", - "PREV", - "HREV", - "DREV", - "OH", - "STOP", - "CMPL", - ] - assert len(asset_status_list.statuses) == len(expected_status_names) - db_status_names = map(lambda x: x.name, asset_status_list.statuses) - db_status_codes = map(lambda x: x.code, asset_status_list.statuses) - assert sorted(expected_status_names) == sorted(db_status_names) - assert sorted(expected_status_codes) == sorted(db_status_codes) - # check if the created_by and updated_by attributes are correctly set - # to the admin - admin = get_admin_user() - for status in asset_status_list.statuses: - assert status.created_by == admin - assert status.updated_by == admin - - -def test_shot_status_list_initialization_when_there_is_a_shot_status_list( - setup_postgresql_db, -): - """Shot statuses created if there is a StatusList for Shot exist.""" - shot_status_list = StatusList.query.filter( - StatusList.name == "Shot Statuses" - ).first() - assert isinstance(shot_status_list, StatusList) - expected_status_names = [ - "Waiting For Dependency", - "Ready To Start", - "Work In Progress", - "Pending Review", - "Has Revision", - "Dependency Has Revision", - "On Hold", - "Stopped", - "Completed", - ] - expected_status_codes = [ - "WFD", - "RTS", - "WIP", - "PREV", - "HREV", - "DREV", - "OH", - "STOP", - "CMPL", - ] - assert len(shot_status_list.statuses) == len(expected_status_names) - db_status_names = map(lambda x: x.name, shot_status_list.statuses) - db_status_codes = map(lambda x: x.code, shot_status_list.statuses) - assert sorted(expected_status_names) == sorted(db_status_names) - assert sorted(expected_status_codes) == sorted(db_status_codes) - # check if the created_by and updated_by attributes are correctly set - # to the admin - admin = get_admin_user() - for status in shot_status_list.statuses: - assert status.created_by == admin - assert status.updated_by == admin + # we do not generate a specific StatusList for Scenes anymore + # as Task specific StatusLists can be used. + assert scene_status_list is None -def test_sequence_status_list_initialization_when_there_is_a_sequence_status_list( - setup_postgresql_db, -): - """Sequence statuses correctly created if a StatusList for Sequence exists.""" - sequence_status_list = StatusList.query.filter( - StatusList.name == "Sequence Statuses" - ).first() - assert isinstance(sequence_status_list, StatusList) - expected_status_names = [ - "Waiting For Dependency", - "Ready To Start", - "Work In Progress", - "Pending Review", - "Has Revision", - "Dependency Has Revision", - "On Hold", - "Stopped", - "Completed", - ] - expected_status_codes = [ - "WFD", - "RTS", - "WIP", - "PREV", - "HREV", - "DREV", - "OH", - "STOP", - "CMPL", - ] - assert len(sequence_status_list.statuses) == len(expected_status_names) - - db_status_names = map(lambda x: x.name, sequence_status_list.statuses) - db_status_codes = map(lambda x: x.code, sequence_status_list.statuses) - assert sorted(expected_status_names) == sorted(db_status_names) - assert sorted(expected_status_codes) == sorted(db_status_codes) - - # check if the created_by and updated_by attributes are correctly set - # to the admin - admin = get_admin_user() - for status in sequence_status_list.statuses: - assert status.created_by == admin - assert status.updated_by == admin +def test_variant_status_list_initialization(setup_postgresql_db): + """Variant statuses are correctly created.""" + variant_status_list = ( + StatusList.query.filter(StatusList.target_entity_type == "Variant").first() + ) + # we do not generate a specific StatusList for Variant anymore + # as Task specific StatusLists can be used. + assert variant_status_list is None def test_review_status_list_initialization(setup_postgresql_db): @@ -4127,7 +3856,10 @@ def test_persistence_of_task(setup_postgresql_db): assert sorted(tasks, key=lambda x: x.name) == sorted( task1_db.tasks, key=lambda x: x.name ) - assert [child_task1, child_task2] == tasks + assert len([child_task1, child_task2]) == len(tasks) + assert sorted([child_task1, child_task2], key=lambda x: x.name) == sorted( + tasks, key=lambda x: x.name + ) assert task1_db.type == type_ assert task1_db.updated_by == updated_by assert task1_db.versions == versions diff --git a/tests/mixins/test_status_mixin.py b/tests/mixins/test_status_mixin.py index b336260e..e6f6912c 100644 --- a/tests/mixins/test_status_mixin.py +++ b/tests/mixins/test_status_mixin.py @@ -24,6 +24,20 @@ def __init__(self, **kwargs): StatusMixin.__init__(self, **kwargs) +class StatMixDerivedClass(StatMixClass): + """A class deriving from StatMixClass. + + With the new approach it should be possible to use the StatusLists created + for the StatMixClass. + """ + + __tablename__ = "StatMixDerivedClasses" + __mapper_args__ = {"polymorphic_identity": "StatMixDerivedClass"} + StatMixDerivedClass_id: Mapped[int] = mapped_column( + "id", ForeignKey("StatMixClasses.id"), primary_key=True + ) + + @pytest.fixture(scope="function") def status_mixin_tests(): """Set up the tests for the StatusMixin class. @@ -83,7 +97,7 @@ def status_mixin_tests(): return data -def test_status_list_argument_is_not_a_status_list_instance(status_mixin_tests): +def test_status_list_arg_is_not_a_status_list_instance(status_mixin_tests): """TypeError is raised if status_list arg is not a StatusList.""" data = status_mixin_tests data["kwargs"]["status_list"] = 100 @@ -96,9 +110,7 @@ def test_status_list_argument_is_not_a_status_list_instance(status_mixin_tests): ) -def test_status_list_attribute_set_to_something_other_than_status_list( - status_mixin_tests, -): +def test_status_list_attr_is_not_a_status_list(status_mixin_tests): """TypeError is raised if status_list is not a StatusList.""" data = status_mixin_tests with pytest.raises(TypeError) as cm: @@ -110,7 +122,34 @@ def test_status_list_attribute_set_to_something_other_than_status_list( ) -def test_status_list_argument_suitable_for_the_current_class(status_mixin_tests): +def test_status_list_arg_is_not_suitable_for_the_current_class(status_mixin_tests): + """TypeError is raised if the Status.target_entity_type is not compatible.""" + data = status_mixin_tests + # create a new status list suitable for another class with different + # entity_type + + new_status_list = StatusList( + name="Sequence Statuses", + statuses=[ + Status(name="On Hold", code="OH"), + Status(name="Complete", code="CMPLT"), + ], + target_entity_type="Sequence", + ) + + data["kwargs"]["status_list"] = new_status_list + data["kwargs"].pop("status") + with pytest.raises(TypeError) as cm: + _ = StatMixClass(**data["kwargs"]) + + assert ( + str(cm.value) + == "The given StatusLists' target_entity_type is Sequence, whereas " + "the entity_type of this object is StatMixClass" + ) + + +def test_status_list_attr_is_not_suitable_for_the_current_class(status_mixin_tests): """TypeError is raised if the Status.target_entity_type is not compatible.""" data = status_mixin_tests # create a new status list suitable for another class with different @@ -135,7 +174,17 @@ def test_status_list_argument_suitable_for_the_current_class(status_mixin_tests) ) -def test_status_list_attribute_is_working_as_expected(status_mixin_tests): +def test_status_list_arg_is_suitable_for_the_super(status_mixin_tests): + """It is possible to use a StatusList that is suitable for a super.""" + data = status_mixin_tests + # use the status list suitable for the super class + # this should not raise a TypeError + assert data["kwargs"]["status_list"].target_entity_type != "StatMixDerivedClass" + obj = StatMixDerivedClass(**data["kwargs"]) + assert obj.status_list == data["kwargs"]["status_list"] + + +def test_status_list_attr_is_working_as_expected(status_mixin_tests): """status_list attribute is working as expected.""" data = status_mixin_tests new_suitable_list = StatusList( @@ -147,13 +196,12 @@ def test_status_list_attribute_is_working_as_expected(status_mixin_tests): target_entity_type="StatMixClass", ) - # this shouldn't raise any error + # this shouldn't raise any errors data["test_mixed_obj"].status_list = new_suitable_list - assert data["test_mixed_obj"].status_list == new_suitable_list -def test_status_argument_set_to_none(status_mixin_tests): +def test_status_arg_set_to_none(status_mixin_tests): """first in the status_list attribute is used if the status arg is None.""" data = status_mixin_tests data["kwargs"]["status"] = None @@ -161,14 +209,14 @@ def test_status_argument_set_to_none(status_mixin_tests): assert new_obj.status == new_obj.status_list[0] -def test_status_attribute_set_to_none(status_mixin_tests): +def test_status_attr_set_to_none(status_mixin_tests): """first in the status_list is used if status attribute is set to None.""" data = status_mixin_tests data["test_mixed_obj"].status = None assert data["test_mixed_obj"].status == data["test_mixed_obj"].status_list[0] -def test_status_argument_is_not_a_status_instance_or_integer(status_mixin_tests): +def test_status_arg_is_not_a_status_instance_or_integer(status_mixin_tests): """TypeError is raised if status arg is not a Status or int.""" data = status_mixin_tests data["kwargs"]["status"] = "0" @@ -182,7 +230,7 @@ def test_status_argument_is_not_a_status_instance_or_integer(status_mixin_tests) ) -def test_status_attribute_set_to_a_value_other_than_a_status_or_integer( +def test_status_attr_is_not_a_status_or_integer( status_mixin_tests, ): """TypeError is raised if status attribute is set to not Status nor int.""" @@ -197,7 +245,7 @@ def test_status_attribute_set_to_a_value_other_than_a_status_or_integer( ) -def test_status_attribute_is_set_to_a_status_which_is_not_in_the_status_list( +def test_status_attr_is_set_to_a_status_which_is_not_in_the_status_list( status_mixin_tests, ): """ValueError is raised if Status is not in the related StatusList.""" @@ -212,7 +260,7 @@ def test_status_attribute_is_set_to_a_status_which_is_not_in_the_status_list( ) -def test_status_argument_is_working_as_expected_with_status_instances( +def test_status_arg_is_working_as_expected_with_status_instances( status_mixin_tests, ): """status attribute value is set correctly with Status arg value.""" @@ -223,7 +271,7 @@ def test_status_argument_is_working_as_expected_with_status_instances( assert new_obj.status == test_value -def test_status_attribute_is_working_as_expected_with_status_instances( +def test_status_attr_is_working_as_expected_with_status_instances( status_mixin_tests, ): """status attribute is working as expected with Status instances.""" @@ -233,7 +281,7 @@ def test_status_attribute_is_working_as_expected_with_status_instances( assert data["test_mixed_obj"].status == test_value -def test_status_argument_is_working_as_expected_with_integers(status_mixin_tests): +def test_status_arg_is_working_as_expected_with_integers(status_mixin_tests): """status attribute value is set correctly with int arg value.""" data = status_mixin_tests data["kwargs"]["status"] = 1 @@ -242,7 +290,7 @@ def test_status_argument_is_working_as_expected_with_integers(status_mixin_tests assert new_obj.status == test_value -def test_status_attribute_is_working_as_expected_with_integers(status_mixin_tests): +def test_status_attr_is_working_as_expected_with_integers(status_mixin_tests): """status attribute is working as expected with integers.""" data = status_mixin_tests test_value = 1 @@ -252,7 +300,7 @@ def test_status_attribute_is_working_as_expected_with_integers(status_mixin_test ) -def test_status_argument_is_an_integer_but_out_of_range(status_mixin_tests): +def test_status_arg_is_an_integer_but_out_of_range(status_mixin_tests): """ValueError is raised if the status argument is out of range.""" data = status_mixin_tests data["kwargs"]["status"] = 10 @@ -265,7 +313,7 @@ def test_status_argument_is_an_integer_but_out_of_range(status_mixin_tests): ) -def test_status_attribute_set_to_an_integer_but_out_of_range(status_mixin_tests): +def test_status_attr_set_to_an_integer_but_out_of_range(status_mixin_tests): """ValueError is raised if the status attribute is set to out of range int.""" data = status_mixin_tests with pytest.raises(ValueError) as cm: @@ -277,7 +325,7 @@ def test_status_attribute_set_to_an_integer_but_out_of_range(status_mixin_tests) ) -def test_status_argument_is_a_negative_integer(status_mixin_tests): +def test_status_arg_is_a_negative_integer(status_mixin_tests): """ValueError will be raised if the status argument is a negative int.""" data = status_mixin_tests data["kwargs"]["status"] = -10 @@ -287,7 +335,7 @@ def test_status_argument_is_a_negative_integer(status_mixin_tests): assert str(cm.value) == "StatMixClass.status must be a non-negative integer" -def test_status_attribute_set_to_an_negative_integer(status_mixin_tests): +def test_status_attr_set_to_an_negative_integer(status_mixin_tests): """ValueError is raised if the status attribute is set to a negative int.""" data = status_mixin_tests with pytest.raises(ValueError) as cm: @@ -310,6 +358,16 @@ def __init__(self, **kwargs): StatusMixin.__init__(self, **kwargs) +class StatusListAutoAddDerivedClass(StatusListAutoAddClass): + """A class derived from StatusListAutoAddClass for testing purposes.""" + + __tablename__ = "StatusListAutoAddDerivedClass" + __mapper_args__ = {"polymorphic_identity": "StatusListAutoAddDerivedClass"} + statusListAutoAddClass_id: Mapped[int] = mapped_column( + "id", ForeignKey("StatusListAutoAddClass.id"), primary_key=True + ) + + class StatusListNoAutoAddClass(SimpleEntity, StatusMixin): """A class derived from stalker.core.models.SimpleEntity for testing purposes.""" @@ -383,7 +441,7 @@ def setup_status_mixin_db_tests(setup_postgresql_db): return data -def test_status_list_attribute_is_skipped_and_there_is_a_db_setup( +def test_status_list_arg_is_skipped_and_there_is_a_db_setup( setup_status_mixin_db_tests, ): """no error raised, status_list is filled with StatusList instance, with db.""" @@ -412,7 +470,7 @@ def test_status_list_attribute_is_skipped_and_there_is_a_db_setup( assert test_status_list_auto_add_class.status_list == test_status_list -def test_status_list_attribute_is_skipped_and_there_is_a_db_setup_but_no_suitable_status_list( +def test_status_list_arg_is_skipped_and_there_is_a_db_setup_but_no_suitable_status_list( setup_status_mixin_db_tests, ): """TypeError is raised no suitable StatusList in the database.""" @@ -446,7 +504,7 @@ def test_status_list_attribute_is_skipped_and_there_is_a_db_setup_but_no_suitabl ) -def test_status_list_argument_is_none(setup_status_mixin_db_tests): +def test_status_list_arg_is_none(setup_status_mixin_db_tests): """TypeError is raised if trying to initialize status_list with None.""" data = setup_status_mixin_db_tests data["kwargs"]["status_list"] = None @@ -462,7 +520,7 @@ def test_status_list_argument_is_none(setup_status_mixin_db_tests): ) -def test_status_list_argument_skipped(setup_status_mixin_db_tests): +def test_status_list_arg_skipped(setup_status_mixin_db_tests): """TypeError is raised if status_list argument is skipped.""" data = setup_status_mixin_db_tests data["kwargs"].pop("status_list") @@ -478,7 +536,7 @@ def test_status_list_argument_skipped(setup_status_mixin_db_tests): ) -def test_status_list_attribute_set_to_none(setup_status_mixin_db_tests): +def test_status_list_attr_set_to_none(setup_status_mixin_db_tests): """TypeError is raised if trying to set the status_list to None.""" data = setup_status_mixin_db_tests with pytest.raises(TypeError) as cm: @@ -491,3 +549,28 @@ def test_status_list_attribute_set_to_none(setup_status_mixin_db_tests): "(StatusList.target_entity_type=StatMixClass) with the " "'status_list' argument" ) + + +def test_status_list_is_found_automatically_for_derived_class( + setup_status_mixin_db_tests, +): + """StatusList can be found automatically for StatusListAutoAddDerivedClass.""" + data = setup_status_mixin_db_tests + status_list = StatusList( + name="Test Status List", + target_entity_type="StatusListAutoAddClass", + statuses=[ + data["test_status1"], + data["test_status2"], + data["test_status3"], + data["test_status4"], + data["test_status5"], + ], + ) + DBSession.save(status_list) + assert status_list.target_entity_type == "StatusListAutoAddClass" + + assert StatusListAutoAddClass in StatusListAutoAddDerivedClass.__mro__ + + test_obj = StatusListAutoAddDerivedClass() + assert test_obj.status_list == status_list diff --git a/tests/models/test_asset.py b/tests/models/test_asset.py index 08f0816c..11dd8a7f 100644 --- a/tests/models/test_asset.py +++ b/tests/models/test_asset.py @@ -124,7 +124,6 @@ def setup_asset_tests(setup_postgresql_db): "description": "This is a test Asset object", "project": data["project1"], "type": data["asset_type1"], - "status": 0, "responsible": [data["test_user1"]], } data["asset1"] = Asset(**data["kwargs"]) @@ -234,7 +233,7 @@ def test_reference_mixin_initialization(setup_asset_tests): def test_status_mixin_initialization(setup_asset_tests): """StatusMixin part is initialized correctly.""" data = setup_asset_tests - status_list = StatusList.query.filter_by(target_entity_type="Asset").first() + status_list = StatusList.query.filter_by(target_entity_type="Task").first() data["kwargs"]["code"] = "SH12314" data["kwargs"]["status"] = 0 data["kwargs"]["status_list"] = status_list @@ -321,3 +320,49 @@ def test_template_variables_for_asset_itself(setup_asset_tests): "task": data["asset1"], "type": data["asset_type1"], } + + +def test_assets_can_use_task_status_list(): + """It is possible to use TaskStatus lists with Assets.""" + # users + test_user1 = User( + name="User1", login="user1", password="12345", email="user1@user1.com" + ) + # statuses + status_wip = Status(code="WIP", name="Work In Progress") + status_cmpl = Status(code="CMPL", name="Complete") + + # Just create a StatusList for Tasks + task_status_list = StatusList( + statuses=[status_wip, status_cmpl], target_entity_type="Task" + ) + project_status_list = StatusList( + statuses=[status_wip, status_cmpl], target_entity_type="Project" + ) + + # types + commercial_project_type = Type( + name="Commercial Project", + code="commproj", + target_entity_type="Project", + ) + asset_type1 = Type(name="Character", code="char", target_entity_type="Asset") + # project + project1 = Project( + name="Test Project1", + code="tp1", + type=commercial_project_type, + status_list=project_status_list, + ) + # this should now be possible + test_asset = Asset( + name="Test Asset", + code="ta", + description="This is a test Asset object", + project=project1, + type=asset_type1, + status_list=task_status_list, + status=status_wip, + responsible=[test_user1], + ) + assert test_asset.status_list == task_status_list diff --git a/tests/models/test_department.py b/tests/models/test_department.py index 45f9732f..d8cafef7 100644 --- a/tests/models/test_department.py +++ b/tests/models/test_department.py @@ -355,17 +355,17 @@ def test_tjp_id_is_working_as_expected(setup_department_db_tests): def test_to_tjp_is_working_as_expected(setup_department_db_tests): """to_tjp property is working as expected.""" data = setup_department_db_tests - expected_tjp = """resource Department_38 "Department_38" { - resource User_33 "User_33" { + expected_tjp = """resource Department_33 "Department_33" { + resource User_28 "User_28" { efficiency 1.0 } - resource User_34 "User_34" { + resource User_29 "User_29" { efficiency 1.0 } - resource User_35 "User_35" { + resource User_30 "User_30" { efficiency 1.0 } - resource User_36 "User_36" { + resource User_31 "User_31" { efficiency 1.0 } }""" diff --git a/tests/models/test_scene.py b/tests/models/test_scene.py index ecb04aca..2d6ecdb0 100644 --- a/tests/models/test_scene.py +++ b/tests/models/test_scene.py @@ -3,7 +3,17 @@ import pytest -from stalker import Entity, Project, Repository, Scene, Task, Type +from stalker import ( + Entity, + Project, + Repository, + Scene, + Status, + StatusList, + Task, + Type, + User, +) from stalker.db.session import DBSession @@ -187,3 +197,45 @@ def test_can_be_used_in_a_task_hierarchy(setup_scene_db_tests): data["test_scene"].parent = task1 assert data["test_scene"] in task1.children + + +def test_scenes_can_use_task_status_list(): + """It is possible to use TaskStatus lists with Shots.""" + # users + test_user1 = User( + name="User1", login="user1", password="12345", email="user1@user1.com" + ) + # statuses + status_wip = Status(code="WIP", name="Work In Progress") + status_cmpl = Status(code="CMPL", name="Complete") + + # Just create a StatusList for Tasks + task_status_list = StatusList( + statuses=[status_wip, status_cmpl], target_entity_type="Task" + ) + project_status_list = StatusList( + statuses=[status_wip, status_cmpl], target_entity_type="Project" + ) + + # types + commercial_project_type = Type( + name="Commercial Project", + code="commproj", + target_entity_type="Project", + ) + # project + project1 = Project( + name="Test Project1", + code="tp1", + type=commercial_project_type, + status_list=project_status_list, + ) + # sequence + test_scene = Scene( + name="Test Scene", + code="tsce", + project=project1, + status_list=task_status_list, + responsible=[test_user1], + ) + assert test_scene.status_list == task_status_list diff --git a/tests/models/test_sequence.py b/tests/models/test_sequence.py index 95f993fc..59efdcec 100644 --- a/tests/models/test_sequence.py +++ b/tests/models/test_sequence.py @@ -3,7 +3,18 @@ import pytest -from stalker import Entity, Link, Project, Repository, Sequence, Task, Type +from stalker import ( + Entity, + Link, + Project, + Repository, + Sequence, + Status, + StatusList, + Task, + Type, + User, +) from stalker.db.session import DBSession @@ -243,3 +254,45 @@ def test__hash__is_working_as_expected(setup_sequence_db_tests): result = hash(data["test_sequence"]) assert isinstance(result, int) assert result == data["test_sequence"].__hash__() + + +def test_sequences_can_use_task_status_list(): + """It is possible to use TaskStatus lists with Shots.""" + # users + test_user1 = User( + name="User1", login="user1", password="12345", email="user1@user1.com" + ) + # statuses + status_wip = Status(code="WIP", name="Work In Progress") + status_cmpl = Status(code="CMPL", name="Complete") + + # Just create a StatusList for Tasks + task_status_list = StatusList( + statuses=[status_wip, status_cmpl], target_entity_type="Task" + ) + project_status_list = StatusList( + statuses=[status_wip, status_cmpl], target_entity_type="Project" + ) + + # types + commercial_project_type = Type( + name="Commercial Project", + code="commproj", + target_entity_type="Project", + ) + # project + project1 = Project( + name="Test Project1", + code="tp1", + type=commercial_project_type, + status_list=project_status_list, + ) + # sequence + test_seq1 = Sequence( + name="Test Sequence", + code="tseq", + project=project1, + status_list=task_status_list, + responsible=[test_user1], + ) + assert test_seq1.status_list == task_status_list diff --git a/tests/models/test_shot.py b/tests/models/test_shot.py index 021cb672..d1e22090 100644 --- a/tests/models/test_shot.py +++ b/tests/models/test_shot.py @@ -18,6 +18,7 @@ StatusList, Task, Type, + User, ) from stalker.db.session import DBSession @@ -1542,3 +1543,44 @@ def test_template_variables_include_sequence_for_shots(setup_shot_db_tests): assert "sequence" in template_variables assert data["test_shot"].sequence is not None assert template_variables["sequence"] == data["test_shot"].sequence + + +def test_shots_can_use_task_status_list(): + """It is possible to use TaskStatus lists with Shots.""" + # users + test_user1 = User( + name="User1", login="user1", password="12345", email="user1@user1.com" + ) + # statuses + status_wip = Status(code="WIP", name="Work In Progress") + status_cmpl = Status(code="CMPL", name="Complete") + + # Just create a StatusList for Tasks + task_status_list = StatusList( + statuses=[status_wip, status_cmpl], target_entity_type="Task" + ) + project_status_list = StatusList( + statuses=[status_wip, status_cmpl], target_entity_type="Project" + ) + + # types + commercial_project_type = Type( + name="Commercial Project", + code="commproj", + target_entity_type="Project", + ) + # project + project1 = Project( + name="Test Project1", + code="tp1", + type=commercial_project_type, + status_list=project_status_list, + ) + # shots + test_shot1 = Shot( + code="TestSH001", + project=project1, + status_list=task_status_list, + responsible=[test_user1], + ) + assert test_shot1.status_list == task_status_list diff --git a/tests/models/test_task.py b/tests/models/test_task.py index 9fb3628c..1cdea655 100644 --- a/tests/models/test_task.py +++ b/tests/models/test_task.py @@ -4798,9 +4798,10 @@ def test_percent_complete_attr_is_not_using_any_time_logs_for_a_duration_task( new_task.computed_start = now + td(days=1) new_task.computed_end = now + td(days=2) + resource1 = new_task.resources[0] _ = TimeLog( task=new_task, - resource=new_task.resources[0], + resource=resource1, start=now + td(days=1), end=now + td(days=2), ) @@ -4833,9 +4834,11 @@ def test_percent_complete_attr_is_working_as_expected_for_a_container_task( DBSession.add(parent_task) new_task.time_logs = [] + resource1 = new_task.resources[0] + resource2 = new_task.resources[1] tlog1 = TimeLog( task=new_task, - resource=new_task.resources[0], + resource=resource1, start=now - td(hours=4), end=now - td(hours=2), ) @@ -4845,7 +4848,7 @@ def test_percent_complete_attr_is_working_as_expected_for_a_container_task( tlog2 = TimeLog( task=new_task, - resource=new_task.resources[1], + resource=resource2, start=now - td(hours=4), end=now + td(hours=1), ) @@ -4887,9 +4890,11 @@ def test_percent_complete_attr_is_working_as_expected_for_a_container_task_with_ DBSession.add(parent_task) new_task.time_logs = [] + resource1 = new_task.resources[0] + resource2 = new_task.resources[1] tlog1 = TimeLog( task=new_task, - resource=new_task.resources[0], + resource=resource1, start=now - td(hours=4), end=now - td(hours=2), ) @@ -4898,7 +4903,7 @@ def test_percent_complete_attr_is_working_as_expected_for_a_container_task_with_ tlog2 = TimeLog( task=new_task, - resource=new_task.resources[1], + resource=resource2, start=now - td(hours=4), end=now + td(hours=1), ) @@ -4941,9 +4946,11 @@ def test_percent_complete_attr_is_working_as_expected_for_a_container_task_with_ parent_task = Task(**kwargs) new_task.time_logs = [] + resource1 = new_task.resources[0] + resource2 = new_task.resources[1] tlog1 = TimeLog( task=new_task, - resource=new_task.resources[0], + resource=resource1, start=now - td(hours=4), end=now - td(hours=2), ) @@ -4953,7 +4960,7 @@ def test_percent_complete_attr_is_working_as_expected_for_a_container_task_with_ tlog2 = TimeLog( task=new_task, - resource=new_task.resources[1], + resource=resource2, start=now - td(hours=4), end=now + td(hours=1), ) @@ -5135,8 +5142,12 @@ def test_percent_complete_attr_is_working_as_expected_for_a_leaf_task( now = dt.now(pytz.utc) new_task.time_logs = [] + # we can't use new_task.resources list directly between commits, + # as apparently the order is changing after a TimeLog is created + resource1 = new_task.resources[0] + resource2 = new_task.resources[1] tlog1 = TimeLog( - task=new_task, resource=new_task.resources[0], start=now, end=now + td(hours=8) + task=new_task, resource=resource1, start=now, end=now + td(hours=8) ) DBSession.add(tlog1) DBSession.commit() @@ -5144,7 +5155,7 @@ def test_percent_complete_attr_is_working_as_expected_for_a_leaf_task( assert tlog1 in new_task.time_logs tlog2 = TimeLog( - task=new_task, resource=new_task.resources[1], start=now, end=now + td(hours=12) + task=new_task, resource=resource2, start=now, end=now + td(hours=12) ) DBSession.add(tlog2) DBSession.commit() @@ -5255,8 +5266,12 @@ def test_total_logged_seconds_is_the_sum_of_all_time_logs(setup_task_db_tests): td = datetime.timedelta now = dt.now(pytz.utc) new_task.time_logs = [] + + # apparently the new_task.resources order is changing between commits. + resource1 = new_task.resources[0] + resource2 = new_task.resources[1] tlog1 = TimeLog( - task=new_task, resource=new_task.resources[0], start=now, end=now + td(hours=8) + task=new_task, resource=resource1, start=now, end=now + td(hours=8) ) DBSession.add(tlog1) DBSession.commit() @@ -5264,7 +5279,7 @@ def test_total_logged_seconds_is_the_sum_of_all_time_logs(setup_task_db_tests): assert tlog1 in new_task.time_logs tlog2 = TimeLog( - task=new_task, resource=new_task.resources[1], start=now, end=now + td(hours=12) + task=new_task, resource=resource2, start=now, end=now + td(hours=12) ) DBSession.add(tlog2) DBSession.commit() @@ -5289,14 +5304,16 @@ def test_total_logged_seconds_calls_update_schedule_info( parent_task = Task(**kwargs) new_task.parent = parent_task new_task.time_logs = [] + resource1 = new_task.resources[0] + resource2 = new_task.resources[1] tlog1 = TimeLog( - task=new_task, resource=new_task.resources[0], start=now, end=now + td(hours=8) + task=new_task, resource=resource1, start=now, end=now + td(hours=8) ) DBSession.add(tlog1) DBSession.commit() assert tlog1 in new_task.time_logs tlog2 = TimeLog( - task=new_task, resource=new_task.resources[1], start=now, end=now + td(hours=12) + task=new_task, resource=resource2, start=now, end=now + td(hours=12) ) DBSession.add(tlog2) DBSession.commit() @@ -5326,8 +5343,11 @@ def test_update_schedule_info_on_a_container_of_containers_task( new_task.parent = parent_task parent_task.parent = root_task new_task.time_logs = [] + # apparently the new_task.resources order is changing between commits. + resource1 = new_task.resources[0] + resource2 = new_task.resources[1] tlog1 = TimeLog( - task=new_task, resource=new_task.resources[0], start=now, end=now + td(hours=8) + task=new_task, resource=resource1, start=now, end=now + td(hours=8) ) DBSession.add(new_task) DBSession.add(parent_task) @@ -5336,7 +5356,7 @@ def test_update_schedule_info_on_a_container_of_containers_task( DBSession.commit() assert tlog1 in new_task.time_logs tlog2 = TimeLog( - task=new_task, resource=new_task.resources[1], start=now, end=now + td(hours=12) + task=new_task, resource=resource2, start=now, end=now + td(hours=12) ) DBSession.add(tlog2) DBSession.commit() @@ -5372,14 +5392,17 @@ def test_total_logged_seconds_is_the_sum_of_all_time_logs_of_children( parent_task = Task(**kwargs) new_task.parent = parent_task new_task.time_logs = [] + # apparently the new_task.resources order is changing between commits. + resource1 = new_task.resources[0] + resource2 = new_task.resources[1] tlog1 = TimeLog( - task=new_task, resource=new_task.resources[0], start=now, end=now + td(hours=8) + task=new_task, resource=resource1, start=now, end=now + td(hours=8) ) DBSession.add(tlog1) DBSession.commit() assert tlog1 in new_task.time_logs tlog2 = TimeLog( - task=new_task, resource=new_task.resources[1], start=now, end=now + td(hours=12) + task=new_task, resource=resource2, start=now, end=now + td(hours=12) ) DBSession.add(tlog2) DBSession.commit() @@ -5436,9 +5459,14 @@ def test_total_logged_seconds_is_the_sum_of_all_time_logs_of_children_deeper( parent_task1.parent = parent_task2 assert parent_task2.total_logged_seconds == 10 * 3600 + # we can't use new_task.resources list directly between commits, + # as apparently the order is changing after a TimeLog is created + resource1 = new_task.resources[0] + resource2 = new_task.resources[1] + new_task.time_logs = [] tlog2 = TimeLog( - task=new_task, resource=new_task.resources[0], start=now, end=now + td(hours=8) + task=new_task, resource=resource1, start=now, end=now + td(hours=8) ) DBSession.add(tlog2) DBSession.commit() @@ -5449,7 +5477,7 @@ def test_total_logged_seconds_is_the_sum_of_all_time_logs_of_children_deeper( assert parent_task2.total_logged_seconds == 18 * 3600 tlog3 = TimeLog( - task=new_task, resource=new_task.resources[1], start=now, end=now + td(hours=12) + task=new_task, resource=resource2, start=now, end=now + td(hours=12) ) DBSession.add(tlog3) DBSession.commit() @@ -5477,8 +5505,9 @@ def test_remaining_seconds_is_working_as_expected(setup_task_db_tests): new_task = Task(**kwargs) # create a time_log of 2 hours + resource1 = new_task.resources[0] _ = TimeLog( - task=new_task, start=now, duration=td(hours=2), resource=new_task.resources[0] + task=new_task, start=now, duration=td(hours=2), resource=resource1 ) # check assert ( @@ -5496,7 +5525,7 @@ def test_remaining_seconds_is_working_as_expected(setup_task_db_tests): task=new_task, start=now + td(hours=2), end=now + td(days=5), - resource=new_task.resources[0], + resource=resource1, ) # check assert ( @@ -5509,7 +5538,7 @@ def test_remaining_seconds_is_working_as_expected(setup_task_db_tests): task=new_task, start=now + td(days=5), duration=td(hours=2), - resource=new_task.resources[0], + resource=resource1, ) assert ( new_task.remaining_seconds @@ -5526,7 +5555,7 @@ def test_remaining_seconds_is_working_as_expected(setup_task_db_tests): task=new_task, start=now + td(days=6), duration=td(hours=2), - resource=new_task.resources[0], + resource=resource1, ) new_task.time_logs.append(tlog4) @@ -5541,7 +5570,7 @@ def test_remaining_seconds_is_working_as_expected(setup_task_db_tests): task=new_task, start=now + td(days=7), duration=td(weeks=1), - resource=new_task.resources[0], + resource=resource1, ) new_task.time_logs.append(tlog5) @@ -5562,7 +5591,7 @@ def test_remaining_seconds_is_working_as_expected(setup_task_db_tests): task=new_task, start=now + td(days=15), duration=td(days=30), - resource=new_task.resources[0], + resource=resource1, ) new_task.time_logs.append(tlog6) @@ -5583,7 +5612,7 @@ def test_remaining_seconds_is_working_as_expected(setup_task_db_tests): task=new_task, start=now + td(days=55), duration=td(days=30), - resource=new_task.resources[0], + resource=resource1, ) new_task.time_logs.append(tlog8) # check